diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..c5054db --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root=true + +[*] +end_of_line = lf +insert_final_newline = true +charset = utf-8 +indent_size = 2 +indent_style = space +trim_trailing_whitespace= true \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..252ca3e --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,28 @@ +{ + "extends": [ + "airbnb", + "plugin:prettier/recommended" + ], + "plugins": [ + "prettier" + ], + "rules": { + "prettier/prettier": [ + "error", + { + "endOfLine": "auto" + } + ], + "func-names": "off", + "eqeqeq": "off", + "class-methods-use-this": "off" + }, + "env": { + "browser": true, + "es6": true, + "jest": true + }, + "parserOptions": { + "ecmaVersion": 9 + } + } \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..74c449d --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.sol diff linguist-language=Solidity \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0e97b3b --- /dev/null +++ b/.gitignore @@ -0,0 +1,42 @@ +.embark +chains.json +config/development/mnemonic +config/livenet/password +config/production/password +coverage +embarkArtifacts +node_modules +package-lock.json +dist + +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# Editor artifacts +.vscode +.idea +.project + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +/build + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Slither +crytic-export/ diff --git a/.soliumignore b/.soliumignore new file mode 100644 index 0000000..b512c09 --- /dev/null +++ b/.soliumignore @@ -0,0 +1 @@ +node_modules \ No newline at end of file diff --git a/.soliumrc.json b/.soliumrc.json new file mode 100644 index 0000000..dcead38 --- /dev/null +++ b/.soliumrc.json @@ -0,0 +1,22 @@ +{ + "extends": "solium:all", + "plugins": [ + "security" + ], + "rules": { + "security/no-inline-assembly": "off", + "security/no-assign-params": "off", + "quotes": [ + "error", + "double" + ], + "indentation": [ + "error", + 4 + ], + "arg-overflow": [ + "warning", + 3 + ] + } + } \ No newline at end of file diff --git a/Discover_Specification.md b/Discover_Specification.md new file mode 100644 index 0000000..ff3a08f --- /dev/null +++ b/Discover_Specification.md @@ -0,0 +1,179 @@ +# Discover SNT Ranking + +## Summary + +In order to fulfill one of our whitepaper promises, we need a mechanism that uses SNT to curate DApps. While this is not the only mechanism we will make available to users to find interesting and relevant DApps, it is one of the most important, both for SNT utility and because economic mechanisms are at the heart of how we buidl sustainable peer-to-peer networks. + +## Abstract + +We propose using an exponential [bonded curve](https://beta.observablehq.com/@andytudhope/dapp-store-snt-curation-mechanism), which operates only on downvotes, to implement a simple ranking game. It is the most radical market feasible: the more SNT a DApp stakes, the higher it ranks, with one caveat. The more SNT staked, the cheaper it is for the community to move that DApp down the rankings. + +## Motivation + +Token Curated Registries, and other bonded curve implementations try to incentivise the user with some kind of fungible reward token (often with governance rights/requirements attached to it) in order to decentralise the curation of interesting information. However, this creates mental overhead for users (who must manage multiple tokens, all with different on-chain transactions required) and is unlikely to see high adoption. + +Making the ranking algorithm transparent - and giving users an ability to affect it at a small cost to them should they feel very strongly - is potentially a more effective way to achieve decentralised curation. + +## User Stories + +An effective economic ranking mechanism, selected with the option `Ranked by SNT` (one of many filters), answers the following user stories from our [swarm doc](https://github.com/status-im/swarms/blob/master/ideas/317-dapps-store.md). + +1. **I want to be confident a DApp is usable / not a scam.** + 1. Having an economic mechanism ensures that the DApps which rank highly quite literally are those providing the "most value" to the community. This is because SNT staked to rank is locked out of circulation, meaning each SNT stakeholder's own holding of SNT should increase in value. Coincidentally, the more SNT staked in total in the store, the stronger the assurance that any given DApp which ranks highly is useful and not a scam. +2. **As an SNT stakeholder, I would like to signal using SNT that I find a listing useful.** + 1. Achieved by "upvoting" in the UI. Importantly, upvotes do not effect the bonded curve, users simply donate SNT 1-1 directly to the DApp's `balance`. +3. **As an SNT stakeholder, I would like to signal using SNT that I find a listing to be not useful/poor quality/etc.** + 1. Achieved, on an increasingly cheap basis the more well-resourced a DApp is, by "downvoting" in the UI. Uses an exponential bonded curve to mint downvotes. +4. **As a DApp developer, I want to be able to propose/vote my DApp for inclusion.** + 1. Anybody can submit a DApp for inclusion and "vote" on it by calling `upvote` and adding SNT to its `balance`. + +## Specification + +#### Constants +1. `uint total` - total SNT in circulation. +2. `uint ceiling` - most influential parameter for [_shape_ of curves](https://beta.observablehq.com/@andytudhope/dapp-store-snt-curation-mechanism). +3. `uint max` - max SNT that any one DApp can stake. +4. `uint decimals` - the amount of decimal precision to use for the calculating `max`. +5. `uint safeMax` - protect against overflows into infinity in votesMinted. + +#### Data Struct +1. `address developer` - the developer of the DApp, used to send SNT to when `downvote` or `withdraw` is called. +2. `bytes32 id` - a unique identifier for each DApp, potentially with other metadata associated with it, hence the `bytes32`. +3. `bytes metadata` - the name, url, category and IPFS hash of the DApp so that we can resolve it in the store correctly. +4. `uint balance` - keep track of the total staked on each DApp. +5. `uint rate = 1 - (balance/max)` - used to calculate `available` and `votesMinted`. +6. `uint available = balance * rate` - amount of SNT staked a developer can earn back. NB: this is equivalent to the `cost` of all downvotes. +7. `uint votesMinted = available ** (1/rate)` - total downvotes that are "minted". +8. `uint votesCast` - keep track of the downvotes already cast. +9. `uint effectiveBalance = balance - ((votesCast/(1/rate))*(available/votesMinted))`- the Effective Balance each DApp is actually ranked by in the UI. + +### Constructor + +1. Sets the address for the SNT contract based on arg passed in. +1. `uint total == 6804870174` +2. `uint ceiling = 292`, as this means the max is close to 2M SNT, and is a local minima for votesMinted. +4. `uint decimals = 1000000` - We're use 1/100th of the total SNT in circulation as our bound, based mostly on Twitter polls... +3. `uint max = (total * ceiling)/decimals` +5. `uint safeMax = 77 * max / 100` - 77% of the absolute max, due to limitations with bancor's power approximations in Solidity. + +#### Methods + +1. **createDapp** external + 1. params: `(bytes32 _id, uint _amount)` + +Calls internal method `_createDApp`, passing in `msg.sender`, `_id` and `_amount`. + +2. **upvote** external + 1. params:`(bytes32 _id, uint _amount)` + +Calls internal method `_upvote`, passing in `msg.sender`, `_id` and `_amount`. + +3. **downvote** external + 1. params: `bytes32 _id, uint _amount` + +Calls `downvoteCost` to check the `_amount`, then calls internal method `_downvote`, passing in `msg.sender`, `_id` and `_amount`. + +4. **withdraw** external + 1. params: `(bytes32 _id, uint _amount)` + +Allow developers to reduce thier stake/exit the store provided that `_amount <= available`. Recalculate `balance`, `rate`, `available` and `votesMinted`. If `votesCast > votesMinted`, then set them equal so the maths is future-proof, and recalculate `effectiveBalance`. + +Emit event containing new `effectiveBalance`. + +5. **setMetadata** external + 1. params: `(bytes32 _id, bytes calldata _metadata)` + +Checks that the person trying to set/update the metadata is the developer, then updates the metadata associated with the DApp at that `id` so that we can resolve it correctly client side. + +7. **receiveApproval** external + 1. params: `(address _from, uint256 _amount, address _token, bytes _data)` + +Included so that users need only sign one transaction when creating a DApp, upvoting or downvoting. Checks that the token (SNT), sender, and data are correct. Decodes the `_data` using `abiDecodeRegister`, checks the amount is correct and figures out which of the three "payable" functions (`createDApp`, `upvote`, and `downvote`) is being called by looking at the signature. + +2. **upvoteEffect** external views + 1. params: `(bytes32 _id, uint _amount)` + +Mock add `_amount` to `balance`, calculate `mRate`, `mAvailable`, `mVMinted`, and `mEBalance`. + +Returns the difference between `mEBalance` and the actual `effectiveBalance`. + +3. **downvoteCost** public view + 1. params: `(bytes32 _id)` + +Specifying that each downvote must move the DApp down by 1% allows us to calculate the `cost` without integrating anything. Calculate the `votesRequired` to effect the DApp by the specified %. + +Returns `balanceDownBy`, `votesRequired` and `cost`. + +4. **_createDApp** internal + 1. params: `(address _from, bytes32 _id, uint _amount)` + +Accepts some nominal amount of tokens (> 0) and creates a new Data struct with the `_id` passed to it, setting the new struct's `balance` and using that to calculate `balance`, `rate`, `available`, `votesMinted` and `effectiveBalance` (which is == `balance` at first). + +Emit event containing new `effectiveBalance`. + +4. **_upvote** internal + 1. params: `(address _from, bytes32 _id, uint _amount)` + +Transfer SNT directly to the contract, which means donating directly to the DApp's `balance`, no money to the developer. Though the votes don't use a curve, we still need to recalculate `rate`, `available`, `votesMinted` and `effectiveBalance`. + +Emit event containing new `effectiveBalance`. + +4. **_downvote** internal + 1. params: `(address _from, bytes32 _id, uint _amount)` + +Send SNT from user directly to developer in order to downvote. Call `downvoteCost` to get `balance_down_by`, `votes_required` and `cost`. + +Add `votesRequired` to `votesCast`, recalculate `effectiveBalance`, and subtract `cost` from `available` so that `withdraw` works correctly. + +Emit event containing new `effectiveBalance`. + +8. **abiDecodeRegister** private + 1. params: `(bytes memory _data)` + +Helps decode the data passed to `receiveApproval` using assembly magic. + +## Potential Attacks + +1. **Sybil resistance?** + 1. If I create a lot of accounts for one DApp, will that increase it's ranking? + 2. If I vote for one DApp from lots of different accounts, in small amounts, rather than in 1 big amount from a single account, what effect does it have? + +Creating many accounts for one DApp is not possible - each DApp is uniquely identified and by its `id` and ranked only by the amount of SNT staked on it. In the same way, there is no quadratic effect in this set up, so staking for a DApp from lots of different accounts in small amounts has no greater/lesser effect on its ranking than staking 1 large amount from a single account. + +2. **Incentives to stake bad DApps and "force" the community to spend SNT to downvote?** + +Remember, you never get back more SNT than you stake, so this is also economically sub-optimal. In addition, there will be a free "complaint" feature as part of the "downvote" screen. There is an important difference between "contractual" and "social" (i.e. the Status UI) reality. Status reserves the right to remove from our UI any DApp that actively violates [our principles](https://status.im/contribute/our_principles.html), though anyone else is free to fork the software and implement different social/UI rules for the same contractual reality. This protects even further against any incentive to submit bad/damaging DApps. + +However, at the beginning of the Store, this is an attack vector: ranking highly requires but a small stake, and this could conceivably result in a successful, cheap hype campaign until we change the UI. The price of freedom is eternal vigilance. + +3. **Stake a damaging DApp, force some downvotes, and then withdraw my stake?** + +You can still never earn back quite as much as you initially staked, enforced by the condition in the `withdraw` function: `require(_amount <= available)`. + +4. **What is left in the store when a DApp withdraws the SNT it staked?** + +Simply `balance - available`, i.e. some small amount of SNT not available to be withdrawn. + +## Rationale + +This is a simple economic mechanism that + +1. does not place high mental overheads on users and could conceivably be understood by a wider and non-technical audience and +2. does not require a lot of screen real estate (important on mobile). All that is required is a balance for each DApp and up/downvote carrots to it's right or left, a pattern already well understood on sites like Reddit etc. + +Moreover, having SNT is not required to see (and benefit from) a well-curated list of DApps; only if you want to effect the rankings on that list do you require tokens, which also makes the UX considerably easier for non-technical users. + +From the perspective of DApp Developers - they must still spend some capital to rank well, just as they currently do with SEO and AdWords etc., but _they stand to earn most of that back_ if the community votes on their product/service, and they can withdraw their stake at any time. The algorithm is entirely transparent and they know where they stand and why at all times. + +## Notes + +The beauty of Ethereum to me, can be summed up simply: + +`By deploying immutable contracts to a shared, public computational surface - contracts whose data can be read deterministically by anyone with access to the internet - we can encode idealism into the way we run society.` + +What's more, **what's different this time**, is that the idealism exists independently of the people who encoded it, who inevitably become corrupted, because we are all human. + +However, there is hope in cryptoeconomics, which is not about egalitarianism, but about designing systems with no central point of control. Decentralisation is the goal; egalitarianism is a great success metric. But not the other way around, because egalitarianism is not something for which we can reasonably optimise. + +## Copyright +Copyright and related rights for this specification waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/). \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..00d1ecd --- /dev/null +++ b/README.md @@ -0,0 +1,44 @@ + +# Discover + +Discover new and useful DApps that are mobile-friendly and easy to use. Viewing curated information does not require any special tools, though effecting the way information is ranked will require a web3 wallet, whether that is Status, MetaMask, Trust, Brave or whichever one you prefer. + +## Available Scripts + +This project is based on Embark v4.0.1, with a few things customised for React. Currently, you'll need to run the app and Embark separately, in different tabs in your terminal. + +**`npm run build`** + +Builds the app into the `build` directory. + +**Steps to run the app:** + +* ### `embark run testnet --noserver` + Will connect to the ropsten blockchain and IPFS through Infura + + **Ropsten contracts:** + + 1. SNT - 0x2764b5da3696E3613Ef9864E9B4613f9fA478E75 + 2. Discover - 0x9591a20b9B601651eDF1072A1Dda994C0B1a5bBf + + **Manual needed steps:** + Once embark is running: + 1. In embarkjs.js (row 532) -> change `this._ipfsConnection.id()` to be `this._ipfsConnection.version()` + This is needed because Infura's IPFS has deprecated `id` endpoint, but it was used in embark in order to check if the Infura IPFS API is active.. The workaround above do the same as the deprecated functionality. + 2. In embark.json -> Change the row `"generationDir": "src/embarkArtifacts"` to `"generationDir": "embarkArtifacts"`. In this way you should not need to do step 1 every time you run `embark run testnet`. + +**`npm run start`** + +Runs the app in the development mode. + +Open [http://localhost:3000](http://localhost:3000) to view it in the browser. The page will reload if you make edits. You will also see any lint errors in the console. + + **Important!** If you get `can't establish a connection to a node` error, try to open [http://localhost:3000](http://localhost:3000) in chrome browser. + +### `embark test` + +Will compile your contracts, with hot-reloading, and let you test them locally to your heart's content. + +### slither . --exclude naming-convention --filter-paths token + +Make sure you get TrailofBits' [latest static analysis tool](https://securityonline.info/slither/), and do your own static analysis on the relevant contracts that will be deployed for Discover. \ No newline at end of file diff --git a/config/blockchain.js b/config/blockchain.js new file mode 100644 index 0000000..a6944ec --- /dev/null +++ b/config/blockchain.js @@ -0,0 +1,149 @@ +module.exports = { + // applies to all environments + default: { + 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 + 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 + auto: true, + additionalCors: [], + }, + wsHost: 'localhost', // WS-RPC server listening interface (default: "localhost") + wsPort: 8546, // WS-RPC server listening port (default: 8546) + + // Accounts to use as node accounts + // The order here corresponds to the order of `web3.eth.getAccounts`, so the first one is the `defaultAccount` + // accounts: [ + // { + // nodeAccounts: true, // Accounts use for the node + // numAddresses: '1', // Number of addresses/accounts (defaults to 1) + // password: 'config/development/password', // Password file for the accounts + // }, + // Below are additional accounts that will count as `nodeAccounts` in the `deployment` section of your contract config + // Those will not be unlocked in the node itself + // { + // privateKeyFile: 'path/to/file', // Either a keystore or a list of keys, separated by , or ; + // password: 'passwordForTheKeystore', // Needed to decrypt the keystore file + // }, + // { + // mnemonic: '12 word mnemonic', + // addressIndex: '0', // Optional. The index to start getting the address + // numAddresses: '1', // Optional. The number of addresses to get + // hdpath: "m/44'/60'/0'/0/", // Optional. HD derivation path + // }, + // ], + }, + + // default environment, merges with the settings in default + // assumed to be the intended environment by `embark run` and `embark blockchain` + development: { + ethereumClientName: 'geth', // Can be geth or parity (default:geth) + // ethereumClientBin: "geth", // path to the client binary. Useful if it is not in the global PATH + networkType: 'custom', // Can be: testnet, rinkeby, livenet or custom, in which case, it will use the specified networkId + 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) + 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. + }, + + // merges with the settings in default + // used with "embark run privatenet" and/or "embark blockchain privatenet" + privatenet: { + networkType: 'custom', + networkId: 1337, + isDev: false, + datadir: '.embark/privatenet/datadir', + // -- mineWhenNeeded -- + // 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, + // -- 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 + // 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 + nodiscover: true, + maxpeers: 0, + proxy: true, + accounts: [ + { + nodeAccounts: true, + password: 'config/privatenet/password', // Password to unlock the account + }, + ], + targetGasLimit: 8000000, + simulatorBlocktime: 0, + }, + + privateparitynet: { + 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', + mineWhenNeeded: false, + nodiscover: true, + maxpeers: 0, + proxy: true, + accounts: [ + { + nodeAccounts: true, + password: 'config/privatenet/password', + }, + ], + targetGasLimit: 8000000, + simulatorBlocktime: 0, + }, + + // merges with the settings in default + // used with "embark run testnet" and/or "embark blockchain testnet" + testnet: { + networkType: 'testnet', + syncMode: 'light', + accounts: [ + { + nodeAccounts: true, + 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', + accounts: [ + { + nodeAccounts: true, + 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: { + // } +} diff --git a/config/communication.js b/config/communication.js new file mode 100644 index 0000000..c401dcd --- /dev/null +++ b/config/communication.js @@ -0,0 +1,46 @@ +module.exports = { + // default applies to all environments + default: { + enabled: true, + provider: "whisper", // Communication provider. Currently, Embark only supports whisper + available_providers: ["whisper"], // Array of available providers + }, + + // default environment, merges with the settings in default + // assumed to be the intended environment by `embark run` + development: { + connection: { + host: "localhost", // Host of the blockchain node + port: 8546, // Port of the blockchain node + type: "ws" // Type of connection (ws or rpc) + } + }, + + // merges with the settings in default + // used with "embark run privatenet" + privatenet: { + }, + + // merges with the settings in default + // used with "embark run testnet" + testnet: { + }, + + // merges with the settings in default + // used with "embark run livenet" + livenet: { + }, + + // you can name an environment with specific settings and then specify with + // "embark run custom_name" + //custom_name: { + //} + // Use this section when you need a specific symmetric or private keys in whisper + /* + ,keys: { + symmetricKey: "your_symmetric_key",// Symmetric key for message decryption + privateKey: "your_private_key" // Private Key to be used as a signing key and for message decryption + } + */ + //} +}; diff --git a/config/contracts.js b/config/contracts.js new file mode 100644 index 0000000..ec2727b --- /dev/null +++ b/config/contracts.js @@ -0,0 +1,137 @@ +const wallet = require('./development/mnemonic') + +module.exports = { + // default applies to all environments + default: { + // Blockchain node to deploy the contracts + deployment: { + host: 'localhost', // Host of the blockchain node + port: 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: [ + { + privateKey: "your_private_key", + balance: "5 ether" // You can set the balance of the account in the dev environment + // Balances are in Wei, but you can specify the unit with its name + }, + { + 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 + }, + { + "nodeAccounts": true // Uses the Ethereum node's accounts + } + ] */ + + accounts: [ + { + mnemonic: wallet.mnemonic, + balance: '1534983463450 ether', + }, + ], + }, + // order of connections the dapp should connect to + // dappConnection: [ + // '$WEB3', // uses pre existing web3 object if available (e.g in Mist) + // 'ws://localhost:8546', + // 'http://localhost:8545', + // ], + + // Automatically call `ethereum.enable` if true. + // If false, the following code must run before sending any transaction: `await EmbarkJS.enableEthereum();` + // Default value is true. + // dappAutoEnable: true, + + gas: 'auto', + + // Strategy for the deployment of the contracts: + // - implicit will try to deploy all the contracts located inside the contracts directory + // or the directory configured for the location of the contracts. This is default one + // when not specified + // - explicit will only attempt to deploy the contracts that are explicitly specified inside the + // contracts section. + // strategy: 'implicit', + + // contracts: { + // Discover: { + // args: { _SNT: '0x744d70fdbe2ba4cf95131626614a1763df805b9e' }, + // }, + // MiniMeToken: { deploy: false }, + // TestBancorFormula: { deploy: false }, + // }, + + contracts: { + MiniMeToken: { deploy: false }, + BancorFormula: { deploy: false }, + MiniMeTokenFactory: { deploy: false }, + SafeMath: { deploy: false }, + TestBancorFormula: { deploy: false }, + SNT: { + instanceOf: 'MiniMeToken', + address: '0x2764b5da3696E3613Ef9864E9B4613f9fA478E75', + }, + Discover: { address: '0x9591a20b9B601651eDF1072A1Dda994C0B1a5bBf' }, + // SNT: { + // instanceOf: 'MiniMeToken', + // args: [ + // '$MiniMeTokenFactory', + // '0x0000000000000000000000000000000000000000', + // 0, + // 'TestMiniMeToken', + // 18, + // 'SNT', + // true, + // ], + // }, + // Discover: { + // args: ['$SNT'], + // }, + }, + }, + + // default environment, merges with the settings in default + // assumed to be the intended environment by `embark run` + development: { + dappConnection: [ + 'ws://localhost:8546', + 'http://localhost:8545', + '$WEB3', // uses pre existing web3 object if available (e.g in Mist) + ], + }, + + // merges with the settings in default + // used with "embark run privatenet" + privatenet: {}, + + // merges with the settings in default + // used with "embark run testnet" + testnet: { + deployment: { + accounts: [{ mnemonic: wallet.mnemonic }], + host: `ropsten.infura.io/v3/8675214b97b44e96b70d05326c61fd6a`, + port: false, + type: 'rpc', + protocol: 'https', + }, + dappConnection: [ + 'https://ropsten.infura.io/v3/8675214b97b44e96b70d05326c61fd6a', + ], + }, + + // merges with the settings in default + // used with "embark run 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: { + // } +} diff --git a/config/development/mnemonic.js b/config/development/mnemonic.js new file mode 100644 index 0000000..c95294e --- /dev/null +++ b/config/development/mnemonic.js @@ -0,0 +1,2 @@ +module.exports.mnemonic = + '' diff --git a/config/development/password b/config/development/password new file mode 100644 index 0000000..fca906b --- /dev/null +++ b/config/development/password @@ -0,0 +1 @@ +dev_password \ No newline at end of file diff --git a/config/namesystem.js b/config/namesystem.js new file mode 100644 index 0000000..f3d1446 --- /dev/null +++ b/config/namesystem.js @@ -0,0 +1,39 @@ +module.exports = { + // default applies to all environments + default: { + enabled: true, + available_providers: ["ens"], + provider: "ens" + }, + + // default environment, merges with the settings in default + // assumed to be the intended environment by `embark run` + development: { + register: { + rootDomain: "embark.eth", + subdomains: { + 'status': '0x1a2f3b98e434c02363f3dac3174af93c1d690914' + } + } + }, + + // merges with the settings in default + // used with "embark run privatenet" + privatenet: { + }, + + // merges with the settings in default + // used with "embark run testnet" + testnet: { + }, + + // merges with the settings in default + // used with "embark run 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: { + //} +}; diff --git a/config/pipeline.js b/config/pipeline.js new file mode 100644 index 0000000..7de7b28 --- /dev/null +++ b/config/pipeline.js @@ -0,0 +1,27 @@ +// Embark has support for Flow enabled by default in its built-in webpack +// config: type annotations will automatically be stripped out of DApp sources +// without any additional configuration. Note that type checking is not +// performed during builds. + +// To enable Flow type checking refer to the preconfigured template: +// https://github.com/embark-framework/embark-flow-template +// A new DApp can be created from that template with: +// embark new --template flow + +module.exports = { + typescript: false, + // Setting `typescript: true` in this config will disable Flow support in + // Embark's default webpack config and enable TypeScript support: .ts and + // .tsx sources will automatically be transpiled into JavaScript without any + // additional configuration. Note that type checking is not performed during + // builds. + + // To enable TypeScript type checking refer to the preconfigured template: + // https://github.com/embark-framework/embark-typescript-template + // A new DApp can be created from that template with: + // embark new --template typescript + enabled: true + // Setting `enabled: false` in this config will disable Embark's built-in Webpack + // pipeline. The developer will need to use a different frontend build tool, such as + // `create-react-app` or Angular CLI to build their dapp +}; diff --git a/config/privatenet/genesis-parity.json b/config/privatenet/genesis-parity.json new file mode 100644 index 0000000..03f574b --- /dev/null +++ b/config/privatenet/genesis-parity.json @@ -0,0 +1,147 @@ +{ + "name": "DevelopmentChain", + "engine": { + "instantSeal": null + }, + "params": { + "gasLimitBoundDivisor": "0x0400", + "accountStartNonce": "0x0", + "maximumExtraDataSize": "0x20", + "minGasLimit": "0x1388", + "networkID": "0x11", + "registrar": "0x0000000000000000000000000000000000001337", + "eip150Transition": "0x0", + "eip160Transition": "0x0", + "eip161abcTransition": "0x0", + "eip161dTransition": "0x0", + "eip155Transition": "0x0", + "eip98Transition": "0x7fffffffffffff", + "eip86Transition": "0x7fffffffffffff", + "maxCodeSize": 24576, + "maxCodeSizeTransition": "0x0", + "eip140Transition": "0x0", + "eip211Transition": "0x0", + "eip214Transition": "0x0", + "eip658Transition": "0x0", + "wasmActivationTransition": "0x0" + }, + "genesis": { + "seal": { + "generic": "0x0" + }, + "difficulty": "0x20000", + "author": "0x0000000000000000000000000000000000000000", + "timestamp": "0x00", + "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "extraData": "0x", + "gasLimit": "0x7A1200" + }, + "accounts": { + "0000000000000000000000000000000000000001": { + "balance": "1", + "builtin": { + "name": "ecrecover", + "pricing": { + "linear": { + "base": 3000, + "word": 0 + } + } + } + }, + "0000000000000000000000000000000000000002": { + "balance": "1", + "builtin": { + "name": "sha256", + "pricing": { + "linear": { + "base": 60, + "word": 12 + } + } + } + }, + "0000000000000000000000000000000000000003": { + "balance": "1", + "builtin": { + "name": "ripemd160", + "pricing": { + "linear": { + "base": 600, + "word": 120 + } + } + } + }, + "0000000000000000000000000000000000000004": { + "balance": "1", + "builtin": { + "name": "identity", + "pricing": { + "linear": { + "base": 15, + "word": 3 + } + } + } + }, + "0000000000000000000000000000000000000005": { + "balance": "1", + "builtin": { + "name": "modexp", + "activate_at": 0, + "pricing": { + "modexp": { + "divisor": 20 + } + } + } + }, + "0000000000000000000000000000000000000006": { + "balance": "1", + "builtin": { + "name": "alt_bn128_add", + "activate_at": 0, + "pricing": { + "linear": { + "base": 500, + "word": 0 + } + } + } + }, + "0000000000000000000000000000000000000007": { + "balance": "1", + "builtin": { + "name": "alt_bn128_mul", + "activate_at": 0, + "pricing": { + "linear": { + "base": 40000, + "word": 0 + } + } + } + }, + "0000000000000000000000000000000000000008": { + "balance": "1", + "builtin": { + "name": "alt_bn128_pairing", + "activate_at": 0, + "pricing": { + "alt_bn128_pairing": { + "base": 100000, + "pair": 80000 + } + } + } + }, + "0000000000000000000000000000000000001337": { + "balance": "1", + "constructor": "0x606060405233600060006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff160217905550670de0b6b3a764000060035534610000575b612904806100666000396000f3006060604052361561013c576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806306b2ff471461014157806313af40351461018c57806319362a28146101bf5780633f3935d114610248578063432ced04146102b75780634f39ca59146102eb5780636795dbcd1461032457806369fe0e2d146103c857806379ce9fac146103fd5780638da5cb5b1461045557806390b97fc1146104a457806392698814146105245780639890220b1461055d578063ac4e73f914610584578063ac72c12014610612578063c3a358251461064b578063ddca3f43146106c3578063deb931a2146106e6578063df57b74214610747578063e30bd740146107a8578063eadf976014610862578063ef5454d6146108e7578063f25eb5c114610975578063f6d339e414610984575b610000565b3461000057610172600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610a1f565b604051808215151515815260200191505060405180910390f35b34610000576101bd600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610a81565b005b346100005761022e60048080356000191690602001909190803590602001908201803590602001908080601f0160208091040260200160405190810160405280939291908181526020018383808284378201915050505050509190803560001916906020019091905050610ba2565b604051808215151515815260200191505060405180910390f35b346100005761029d600480803590602001908201803590602001908080601f01602080910402602001604051908101604052809392919081815260200183838082843782019150505050505091905050610dc9565b604051808215151515815260200191505060405180910390f35b6102d1600480803560001916906020019091905050611035565b604051808215151515815260200191505060405180910390f35b346100005761030a60048080356000191690602001909190505061115f565b604051808215151515815260200191505060405180910390f35b346100005761038660048080356000191690602001909190803590602001908201803590602001908080601f01602080910402602001604051908101604052809392919081815260200183838082843782019150505050505091905050611378565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b34610000576103e3600480803590602001909190505061140d565b604051808215151515815260200191505060405180910390f35b346100005761043b60048080356000191690602001909190803573ffffffffffffffffffffffffffffffffffffffff169060200190919050506114b4565b604051808215151515815260200191505060405180910390f35b34610000576104626115fb565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b346100005761050660048080356000191690602001909190803590602001908201803590602001908080601f01602080910402602001604051908101604052809392919081815260200183838082843782019150505050505091905050611621565b60405180826000191660001916815260200191505060405180910390f35b34610000576105436004808035600019169060200190919050506116b2565b604051808215151515815260200191505060405180910390f35b346100005761056a611715565b604051808215151515815260200191505060405180910390f35b34610000576105f8600480803590602001908201803590602001908080601f0160208091040260200160405190810160405280939291908181526020018383808284378201915050505050509190803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050611824565b604051808215151515815260200191505060405180910390f35b3461000057610631600480803560001916906020019091905050611d8b565b604051808215151515815260200191505060405180910390f35b34610000576106ad60048080356000191690602001909190803590602001908201803590602001908080601f01602080910402602001604051908101604052809392919081815260200183838082843782019150505050505091905050611dee565b6040518082815260200191505060405180910390f35b34610000576106d0611e83565b6040518082815260200191505060405180910390f35b3461000057610705600480803560001916906020019091905050611e89565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b3461000057610766600480803560001916906020019091905050611ed2565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b34610000576107d9600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050611f1b565b6040518080602001828103825283818151815260200191508051906020019080838360008314610828575b80518252602083111561082857602082019150602081019050602083039250610804565b505050905090810190601f1680156108545780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b34610000576108cd60048080356000191690602001909190803590602001908201803590602001908080601f0160208091040260200160405190810160405280939291908181526020018383808284378201915050505050509190803590602001909190505061200c565b604051808215151515815260200191505060405180910390f35b346100005761095b600480803590602001908201803590602001908080601f0160208091040260200160405190810160405280939291908181526020018383808284378201915050505050509190803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050612236565b604051808215151515815260200191505060405180910390f35b3461000057610982612425565b005b3461000057610a0560048080356000191690602001909190803590602001908201803590602001908080601f0160208091040260200160405190810160405280939291908181526020018383808284378201915050505050509190803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050612698565b604051808215151515815260200191505060405180910390f35b60006000600260008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020805460018160011615610100020316600290049050141590505b919050565b600060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141515610add57610b9f565b8073ffffffffffffffffffffffffffffffffffffffff16600060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff167f70aea8d848e8a90fb7661b227dc522eb6395c3dac71b63cb59edd5c9899b236460405180905060405180910390a380600060006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055505b5b50565b6000833373ffffffffffffffffffffffffffffffffffffffff1660016000836000191660001916815260200190815260200160002060000160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16141515610c1d57610dc1565b82600160008760001916600019168152602001908152602001600020600201856040518082805190602001908083835b60208310610c705780518252602082019150602081019050602083039250610c4d565b6001836020036101000a03801982511681845116808217855250505050505090500191505090815260200160405180910390208160001916905550836040518082805190602001908083835b60208310610cdf5780518252602082019150602081019050602083039250610cbc565b6001836020036101000a038019825116818451168082178552505050505050905001915050604051809103902085600019167fb829c3e412537bbe794c048ccb9e4605bb4aaaa8e4d4c15c1a6e0c2adc1716ea866040518080602001828103825283818151815260200191508051906020019080838360008314610d82575b805182526020831115610d8257602082019150602081019050602083039250610d5e565b505050905090810190601f168015610dae5780820380516001836020036101000a031916815260200191505b509250505060405180910390a3600191505b5b509392505050565b6000813373ffffffffffffffffffffffffffffffffffffffff1660016000836040518082805190602001908083835b60208310610e1b5780518252602082019150602081019050602083039250610df8565b6001836020036101000a03801982511681845116808217855250505050505090500191505060405180910390206000191660001916815260200190815260200160002060010160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16141515610ea45761102f565b82600260003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000209080519060200190828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f10610f2d57805160ff1916838001178555610f5b565b82800160010185558215610f5b579182015b82811115610f5a578251825591602001919060010190610f3f565b5b509050610f8091905b80821115610f7c576000816000905550600101610f64565b5090565b50503373ffffffffffffffffffffffffffffffffffffffff16836040518082805190602001908083835b60208310610fcd5780518252602082019150602081019050602083039250610faa565b6001836020036101000a03801982511681845116808217855250505050505090500191505060405180910390207f098ae8581bb8bd9af1beaf7f2e9f51f31a8e5a8bfada4e303a645d71d9c9192060405180905060405180910390a3600191505b5b50919050565b600081600060016000836000191660001916815260200190815260200160002060000160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1614151561109b57611159565b6003543410156110aa57611158565b3360016000856000191660001916815260200190815260200160002060000160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055503373ffffffffffffffffffffffffffffffffffffffff1683600019167f4963513eca575aba66fdcd25f267aae85958fe6fb97e75fa25d783f1a091a22160405180905060405180910390a3600191505b5b5b50919050565b6000813373ffffffffffffffffffffffffffffffffffffffff1660016000836000191660001916815260200190815260200160002060000160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff161415156111da57611372565b6002600060016000866000191660001916815260200190815260200160002060010160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020805460018160011615610100020316600290046000825580601f1061127c57506112b3565b601f0160209004906000526020600020908101906112b291905b808211156112ae576000816000905550600101611296565b5090565b5b5060016000846000191660001916815260200190815260200160002060006000820160006101000a81549073ffffffffffffffffffffffffffffffffffffffff02191690556001820160006101000a81549073ffffffffffffffffffffffffffffffffffffffff021916905550503373ffffffffffffffffffffffffffffffffffffffff1683600019167fef1961b4d2909dc23643b309bfe5c3e5646842d98c3a58517037ef3871185af360405180905060405180910390a3600191505b5b50919050565b6000600160008460001916600019168152602001908152602001600020600201826040518082805190602001908083835b602083106113cc57805182526020820191506020810190506020830392506113a9565b6001836020036101000a0380198251168184511680821785525050505050509050019150509081526020016040518091039020546001900490505b92915050565b6000600060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614151561146b576114af565b816003819055507f6bbc57480a46553fa4d156ce702beef5f3ad66303b0ed1a5d4cb44966c6584c3826040518082815260200191505060405180910390a1600190505b5b919050565b6000823373ffffffffffffffffffffffffffffffffffffffff1660016000836000191660001916815260200190815260200160002060000160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1614151561152f576115f4565b8260016000866000191660001916815260200190815260200160002060000160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055508273ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1685600019167f7b97c62130aa09acbbcbf7482630e756592496f1759eaf702f469cf64dfb779460405180905060405180910390a4600191505b5b5092915050565b600060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000600160008460001916600019168152602001908152602001600020600201826040518082805190602001908083835b602083106116755780518252602082019150602081019050602083039250611652565b6001836020036101000a03801982511681845116808217855250505050505090500191505090815260200160405180910390205490505b92915050565b6000600060016000846000191660001916815260200190815260200160002060000160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16141590505b919050565b6000600060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614151561177357611821565b7fdef931299fe61d176f949118058530c1f3f539dcb6950b4e372c9b835c33ca073073ffffffffffffffffffffffffffffffffffffffff16316040518082815260200191505060405180910390a13373ffffffffffffffffffffffffffffffffffffffff166108fc3073ffffffffffffffffffffffffffffffffffffffff16319081150290604051809050600060405180830381858888f19350505050151561181b57610000565b600190505b5b90565b60006000836040518082805190602001908083835b6020831061185c5780518252602082019150602081019050602083039250611839565b6001836020036101000a03801982511681845116808217855250505050505090500191505060405180910390203373ffffffffffffffffffffffffffffffffffffffff1660016000836000191660001916815260200190815260200160002060000160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1614151561190157611d83565b846040518082805190602001908083835b602083106119355780518252602082019150602081019050602083039250611912565b6001836020036101000a03801982511681845116808217855250505050505090500191505060405180910390209150600060016000846000191660001916815260200190815260200160002060010160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1614158015611ab4575081600019166002600060016000866000191660001916815260200190815260200160002060010160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000206040518082805460018160011615610100020316600290048015611aa15780601f10611a7f576101008083540402835291820191611aa1565b820191906000526020600020905b815481529060010190602001808311611a8d575b5050915050604051809103902060001916145b15611c79576002600060016000856000191660001916815260200190815260200160002060010160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020805460018160011615610100020316600290046000825580601f10611b5b5750611b92565b601f016020900490600052602060002090810190611b9191905b80821115611b8d576000816000905550600101611b75565b5090565b5b5060016000836000191660001916815260200190815260200160002060010160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16856040518082805190602001908083835b60208310611c1c5780518252602082019150602081019050602083039250611bf9565b6001836020036101000a03801982511681845116808217855250505050505090500191505060405180910390207f12491ad95fd945e444d88a894ffad3c21959880a4dcd8af99d4ae4ffc71d4abd60405180905060405180910390a35b8360016000846000191660001916815260200190815260200160002060010160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055508373ffffffffffffffffffffffffffffffffffffffff16856040518082805190602001908083835b60208310611d215780518252602082019150602081019050602083039250611cfe565b6001836020036101000a03801982511681845116808217855250505050505090500191505060405180910390207f728435a0031f6a04538fcdd24922a7e06bc7bc945db03e83d22122d1bc5f28df60405180905060405180910390a3600192505b5b505092915050565b6000600060016000846000191660001916815260200190815260200160002060010160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16141590505b919050565b6000600160008460001916600019168152602001908152602001600020600201826040518082805190602001908083835b60208310611e425780518252602082019150602081019050602083039250611e1f565b6001836020036101000a0380198251168184511680821785525050505050509050019150509081526020016040518091039020546001900490505b92915050565b60035481565b600060016000836000191660001916815260200190815260200160002060000160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1690505b919050565b600060016000836000191660001916815260200190815260200160002060010160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1690505b919050565b6020604051908101604052806000815250600260008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000208054600181600116156101000203166002900480601f016020809104026020016040519081016040528092919081815260200182805460018160011615610100020316600290048015611fff5780601f10611fd457610100808354040283529160200191611fff565b820191906000526020600020905b815481529060010190602001808311611fe257829003601f168201915b505050505090505b919050565b6000833373ffffffffffffffffffffffffffffffffffffffff1660016000836000191660001916815260200190815260200160002060000160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff161415156120875761222e565b82600102600160008760001916600019168152602001908152602001600020600201856040518082805190602001908083835b602083106120dd57805182526020820191506020810190506020830392506120ba565b6001836020036101000a03801982511681845116808217855250505050505090500191505090815260200160405180910390208160001916905550836040518082805190602001908083835b6020831061214c5780518252602082019150602081019050602083039250612129565b6001836020036101000a038019825116818451168082178552505050505050905001915050604051809103902085600019167fb829c3e412537bbe794c048ccb9e4605bb4aaaa8e4d4c15c1a6e0c2adc1716ea8660405180806020018281038252838181518152602001915080519060200190808383600083146121ef575b8051825260208311156121ef576020820191506020810190506020830392506121cb565b505050905090810190601f16801561221b5780820380516001836020036101000a031916815260200191505b509250505060405180910390a3600191505b5b509392505050565b6000600060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415156122945761241f565b82600260008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000209080519060200190828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f1061231d57805160ff191683800117855561234b565b8280016001018555821561234b579182015b8281111561234a57825182559160200191906001019061232f565b5b50905061237091905b8082111561236c576000816000905550600101612354565b5090565b50508173ffffffffffffffffffffffffffffffffffffffff16836040518082805190602001908083835b602083106123bd578051825260208201915060208101905060208303925061239a565b6001836020036101000a03801982511681845116808217855250505050505090500191505060405180910390207f098ae8581bb8bd9af1beaf7f2e9f51f31a8e5a8bfada4e303a645d71d9c9192060405180905060405180910390a3600190505b5b92915050565b3373ffffffffffffffffffffffffffffffffffffffff16600260003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060405180828054600181600116156101000203166002900480156124d65780601f106124b45761010080835404028352918201916124d6565b820191906000526020600020905b8154815290600101906020018083116124c2575b505091505060405180910390207f12491ad95fd945e444d88a894ffad3c21959880a4dcd8af99d4ae4ffc71d4abd60405180905060405180910390a360016000600260003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060405180828054600181600116156101000203166002900480156125b05780601f1061258e5761010080835404028352918201916125b0565b820191906000526020600020905b81548152906001019060200180831161259c575b505091505060405180910390206000191660001916815260200190815260200160002060010160006101000a81549073ffffffffffffffffffffffffffffffffffffffff0219169055600260003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020805460018160011615610100020316600290046000825580601f1061265d5750612694565b601f01602090049060005260206000209081019061269391905b8082111561268f576000816000905550600101612677565b5090565b5b505b565b6000833373ffffffffffffffffffffffffffffffffffffffff1660016000836000191660001916815260200190815260200160002060000160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16141515612713576128d0565b8273ffffffffffffffffffffffffffffffffffffffff16600102600160008760001916600019168152602001908152602001600020600201856040518082805190602001908083835b6020831061277f578051825260208201915060208101905060208303925061275c565b6001836020036101000a03801982511681845116808217855250505050505090500191505090815260200160405180910390208160001916905550836040518082805190602001908083835b602083106127ee57805182526020820191506020810190506020830392506127cb565b6001836020036101000a038019825116818451168082178552505050505050905001915050604051809103902085600019167fb829c3e412537bbe794c048ccb9e4605bb4aaaa8e4d4c15c1a6e0c2adc1716ea866040518080602001828103825283818151815260200191508051906020019080838360008314612891575b8051825260208311156128915760208201915060208101905060208303925061286d565b505050905090810190601f1680156128bd5780820380516001836020036101000a031916815260200191505b509250505060405180910390a3600191505b5b5093925050505600a165627a7a7230582066b2da4773a0f1d81efe071c66b51c46868a871661efd18c0f629353ff4c1f9b0029" + }, + "00a329c0648769a73afac7f9381e08fb43dbea72": { + "balance": "1606938044258990275541962092341162602522202993782792835301376" + } + } +} \ No newline at end of file diff --git a/config/privatenet/genesis.json b/config/privatenet/genesis.json new file mode 100644 index 0000000..a6c3940 --- /dev/null +++ b/config/privatenet/genesis.json @@ -0,0 +1,20 @@ +{ + "config": { + "homesteadBlock": 0, + "byzantiumBlock": 0, + "eip155Block": 0, + "eip158Block": 0, + "daoForkSupport": true + }, + "nonce": "0x0000000000000042", + "difficulty": "0x0", + "alloc": { + "0x3333333333333333333333333333333333333333": {"balance": "15000000000000000000"} + }, + "mixhash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "coinbase": "0x3333333333333333333333333333333333333333", + "timestamp": "0x00", + "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "extraData": "0x", + "gasLimit": "0x7a1200" +} diff --git a/config/privatenet/password b/config/privatenet/password new file mode 100644 index 0000000..c747d67 --- /dev/null +++ b/config/privatenet/password @@ -0,0 +1 @@ +dev_password diff --git a/config/storage.js b/config/storage.js new file mode 100644 index 0000000..62484a0 --- /dev/null +++ b/config/storage.js @@ -0,0 +1,74 @@ +module.exports = { + // default applies to all environments + default: { + enabled: true, + ipfs_bin: 'ipfs', + provider: 'ipfs', + available_providers: ['ipfs'], + upload: { + host: 'localhost', + port: 5001, + }, + dappConnection: [ + { + provider: 'ipfs', + host: 'localhost', + port: 5001, + getUrl: 'http://localhost:8080/ipfs/', + }, + ], + // Configuration to start Swarm in the same terminal as `embark run` + /* ,account: { + address: "YOUR_ACCOUNT_ADDRESS", // Address of account accessing Swarm + password: "PATH/TO/PASSWORD/FILE" // File containing the password of the account + }, + swarmPath: "PATH/TO/SWARM/EXECUTABLE" // Path to swarm executable (default: swarm) */ + }, + + // default environment, merges with the settings in default + // assumed to be the intended environment by `embark run` + development: { + enabled: true, + upload: { + provider: 'ipfs', + host: 'localhost', + port: 5001, + getUrl: 'http://localhost:8080/ipfs/', + }, + }, + + // merges with the settings in default + // used with "embark run privatenet" + privatenet: {}, + + // merges with the settings in default + // used with "embark run testnet" + testnet: { + enabled: true, + ipfs_bin: 'ipfs', + provider: 'ipfs', + available_providers: ['ipfs'], + upload: { + host: 'localhost', + port: 5001, + }, + dappConnection: [ + { + provider: 'ipfs', + protocol: 'https', + host: 'ipfs.infura.io', + port: 5001, + getUrl: 'https://ipfs.infura.io/ipfs/', + }, + ], + }, + + // merges with the settings in default + // used with "embark run livenet" + livenet: {}, + + // you can name an environment with specific settings and then specify with + // "embark run custom_name" + // custom_name: { + // } +} diff --git a/config/testnet/password b/config/testnet/password new file mode 100644 index 0000000..414f849 --- /dev/null +++ b/config/testnet/password @@ -0,0 +1 @@ +test_password diff --git a/config/webserver.js b/config/webserver.js new file mode 100644 index 0000000..506490b --- /dev/null +++ b/config/webserver.js @@ -0,0 +1,6 @@ +module.exports = { + enabled: true, + host: "localhost", + openBrowser: true, + port: 8000 +}; diff --git a/contracts/.gitkeep b/contracts/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/contracts/Discover.sol b/contracts/Discover.sol new file mode 100644 index 0000000..f10b35a --- /dev/null +++ b/contracts/Discover.sol @@ -0,0 +1,395 @@ +pragma solidity ^0.5.2; + +import "./token/MiniMeTokenInterface.sol"; +import "./token/ApproveAndCallFallBack.sol"; +import "./utils/SafeMath.sol"; +import "./utils/BancorFormula.sol"; + + +contract Discover is ApproveAndCallFallBack, BancorFormula { + using SafeMath for uint; + + // Could be any MiniMe token + MiniMeTokenInterface SNT; + + // Total SNT in circulation + uint public total; + + // Parameter to calculate Max SNT any one DApp can stake + uint public ceiling; + + // The max amount of tokens it is possible to stake, as a percentage of the total in circulation + uint public max; + + // Decimal precision for this contract + uint public decimals; + + // Prevents overflows in votesMinted + uint public safeMax; + + // Whether we need more than an id param to identify arbitrary data must still be discussed. + struct Data { + address developer; + bytes32 id; + bytes32 metadata; + uint balance; + uint rate; + uint available; + uint votesMinted; + uint votesCast; + uint effectiveBalance; + } + + Data[] public dapps; + mapping(bytes32 => uint) public id2index; + mapping(bytes32 => bool) existingIDs; + + event DAppCreated(bytes32 indexed id, uint newEffectiveBalance); + event Upvote(bytes32 indexed id, uint newEffectiveBalance); + event Downvote(bytes32 indexed id, uint newEffectiveBalance); + event Withdraw(bytes32 indexed id, uint newEffectiveBalance); + event MetadataUpdated(bytes32 indexed id); + + constructor(MiniMeTokenInterface _SNT) public { + SNT = _SNT; + + total = 6804870174; + + ceiling = 292; // See here for more: https://observablehq.com/@andytudhope/dapp-store-snt-curation-mechanism + + decimals = 1000000; // 4 decimal points for %, 2 because we only use 1/100th of total in circulation + + max = total.mul(ceiling).div(decimals); + + safeMax = uint(77).mul(max).div(100); // Limited by accuracy of BancorFormula + } + + /** + * @dev Anyone can create a DApp (i.e an arb piece of data this contract happens to care about). + * @param _id bytes32 unique identifier. + * @param _amount of tokens to stake on initial ranking. + * @param _metadata metadata hex string + */ + function createDApp(bytes32 _id, uint _amount, bytes32 _metadata) external { + _createDApp( + msg.sender, + _id, + _amount, + _metadata); + } + + /** + * @dev Sends SNT directly to the contract, not the developer. This gets added to the DApp's balance, no curve required. + * @param _id bytes32 unique identifier. + * @param _amount of tokens to stake on DApp's ranking. Used for upvoting + staking more. + */ + function upvote(bytes32 _id, uint _amount) external { + _upvote(msg.sender, _id, _amount); + } + + /** + * @dev Sends SNT to the developer and lowers the DApp's effective balance by 1% + * @param _id bytes32 unique identifier. + * @param _amount uint, included for approveAndCallFallBack + */ + function downvote(bytes32 _id, uint _amount) external { + _downvote(msg.sender, _id, _amount); + } + + /** + * @dev Developers can withdraw an amount not more than what was available of the + SNT they originally staked minus what they have already received back in downvotes. + * @param _id bytes32 unique identifier. + * @param _amount of tokens to withdraw from DApp's overall balance. + */ + function withdraw(bytes32 _id, uint _amount) external { + Data storage d = _getDAppById(_id); + + require(msg.sender == d.developer, "Only the developer can withdraw SNT staked on this data"); + require(_amount <= d.available, "You can only withdraw a percentage of the SNT staked, less what you have already received"); + + uint precision; + uint result; + + d.balance = d.balance.sub(_amount); + d.rate = decimals.sub(d.balance.mul(decimals).div(max)); + d.available = d.balance.mul(d.rate); + + (result, precision) = BancorFormula.power( + d.available, + decimals, + uint32(decimals), + uint32(d.rate)); + + d.votesMinted = result >> precision; + if (d.votesCast > d.votesMinted) { + d.votesCast = d.votesMinted; + } + + uint temp1 = d.votesCast.mul(d.rate).mul(d.available); + uint temp2 = d.votesMinted.mul(decimals).mul(decimals); + uint effect = temp1.div(temp2); + + d.effectiveBalance = d.balance.sub(effect); + + require(SNT.transfer(d.developer, _amount), "Transfer failed"); + + emit Withdraw(_id, d.effectiveBalance); + } + + /** + * dev Set the content for the dapp + * @param _id bytes32 unique identifier. + * @param _metadata metadata info + */ + function setMetadata(bytes32 _id, bytes32 _metadata) external { + uint dappIdx = id2index[_id]; + Data storage d = dapps[dappIdx]; + require(d.developer == msg.sender, "Only the developer can update the metadata"); + d.metadata = _metadata; + emit MetadataUpdated(_id); + } + + /** + * @dev Used in UI in order to fetch all dapps + * @return dapps count + */ + function getDAppsCount() external view returns(uint) { + return dapps.length; + } + + /** + * @notice Support for "approveAndCall". + * @param _from Who approved. + * @param _amount Amount being approved, needs to be equal `_amount` or `cost`. + * @param _token Token being approved, needs to be `SNT`. + * @param _data Abi encoded data with selector of `register(bytes32,address,bytes32,bytes32)`. + */ + function receiveApproval( + address _from, + uint256 _amount, + address _token, + bytes calldata _data + ) + external + { + require(_token == address(SNT), "Wrong token"); + require(_token == address(msg.sender), "Wrong account"); + require(_data.length <= 196, "Incorrect data"); + + bytes4 sig; + bytes32 id; + uint256 amount; + bytes32 metadata; + + (sig, id, amount, metadata) = abiDecodeRegister(_data); + require(_amount == amount, "Wrong amount"); + + if (sig == bytes4(0x7e38d973)) { + _createDApp( + _from, + id, + amount, + metadata); + } else if (sig == bytes4(0xac769090)) { + _downvote(_from, id, amount); + } else if (sig == bytes4(0x2b3df690)) { + _upvote(_from, id, amount); + } else { + revert("Wrong method selector"); + } + } + + /** + * @dev Used in UI to display effect on ranking of user's donation + * @param _id bytes32 unique identifier. + * @param _amount of tokens to stake/"donate" to this DApp's ranking. + * @return effect of donation on DApp's effectiveBalance + */ + function upvoteEffect(bytes32 _id, uint _amount) external view returns(uint effect) { + Data memory d = _getDAppById(_id); + require(d.balance.add(_amount) <= safeMax, "You cannot upvote by this much, try with a lower amount"); + + // Special case - no downvotes yet cast + if (d.votesCast == 0) { + return _amount; + } + + uint precision; + uint result; + + uint mBalance = d.balance.add(_amount); + uint mRate = decimals.sub(mBalance.mul(decimals).div(max)); + uint mAvailable = mBalance.mul(mRate); + + (result, precision) = BancorFormula.power( + mAvailable, + decimals, + uint32(decimals), + uint32(mRate)); + + uint mVMinted = result >> precision; + + uint temp1 = d.votesCast.mul(mRate).mul(mAvailable); + uint temp2 = mVMinted.mul(decimals).mul(decimals); + uint mEffect = temp1.div(temp2); + + uint mEBalance = mBalance.sub(mEffect); + + return (mEBalance.sub(d.effectiveBalance)); + } + + /** + * @dev Downvotes always remove 1% of the current ranking. + * @param _id bytes32 unique identifier. + * @return balance_down_by, votes_required, cost + */ + function downvoteCost(bytes32 _id) public view returns(uint b, uint vR, uint c) { + Data memory d = _getDAppById(_id); + return _downvoteCost(d); + } + + function _createDApp( + address _from, + bytes32 _id, + uint _amount, + bytes32 _metadata + ) + internal + { + require(!existingIDs[_id], "You must submit a unique ID"); + + require(_amount > 0, "You must spend some SNT to submit a ranking in order to avoid spam"); + require (_amount <= safeMax, "You cannot stake more SNT than the ceiling dictates"); + + uint dappIdx = dapps.length; + + dapps.length++; + + Data storage d = dapps[dappIdx]; + d.developer = _from; + d.id = _id; + d.metadata = _metadata; + + uint precision; + uint result; + + d.balance = _amount; + d.rate = decimals.sub((d.balance).mul(decimals).div(max)); + d.available = d.balance.mul(d.rate); + + (result, precision) = BancorFormula.power( + d.available, + decimals, + uint32(decimals), + uint32(d.rate)); + + d.votesMinted = result >> precision; + d.votesCast = 0; + d.effectiveBalance = _amount; + + id2index[_id] = dappIdx; + existingIDs[_id] = true; + + require(SNT.allowance(_from, address(this)) >= _amount, "Not enough SNT allowance"); + require(SNT.transferFrom(_from, address(this), _amount), "Transfer failed"); + + emit DAppCreated(_id, d.effectiveBalance); + } + + function _upvote(address _from, bytes32 _id, uint _amount) internal { + require(_amount > 0, "You must send some SNT in order to upvote"); + + Data storage d = _getDAppById(_id); + + require(d.balance.add(_amount) <= safeMax, "You cannot upvote by this much, try with a lower amount"); + + uint precision; + uint result; + + d.balance = d.balance.add(_amount); + d.rate = decimals.sub((d.balance).mul(decimals).div(max)); + d.available = d.balance.mul(d.rate); + + (result, precision) = BancorFormula.power( + d.available, + decimals, + uint32(decimals), + uint32(d.rate)); + + d.votesMinted = result >> precision; + + uint temp1 = d.votesCast.mul(d.rate).mul(d.available); + uint temp2 = d.votesMinted.mul(decimals).mul(decimals); + uint effect = temp1.div(temp2); + + d.effectiveBalance = d.balance.sub(effect); + + require(SNT.allowance(_from, address(this)) >= _amount, "Not enough SNT allowance"); + require(SNT.transferFrom(_from, address(this), _amount), "Transfer failed"); + + emit Upvote(_id, d.effectiveBalance); + } + + function _downvote(address _from, bytes32 _id, uint _amount) internal { + Data storage d = _getDAppById(_id); + (uint b, uint vR, uint c) = _downvoteCost(d); + + require(_amount == c, "Incorrect amount: valid iff effect on ranking is 1%"); + + d.available = d.available.sub(_amount); + d.votesCast = d.votesCast.add(vR); + d.effectiveBalance = d.effectiveBalance.sub(b); + + require(SNT.allowance(_from, address(this)) >= _amount, "Not enough SNT allowance"); + require(SNT.transferFrom(_from, address(this), _amount), "Transfer failed"); + require(SNT.transfer(d.developer, _amount), "Transfer failed"); + + emit Downvote(_id, d.effectiveBalance); + } + + function _downvoteCost(Data memory d) internal view returns(uint b, uint vR, uint c) { + uint balanceDownBy = (d.effectiveBalance.div(100)); + uint votesRequired = (balanceDownBy.mul(d.votesMinted).mul(d.rate)).div(d.available); + uint votesAvailable = d.votesMinted.sub(d.votesCast).sub(votesRequired); + uint temp = (d.available.div(votesAvailable)).mul(votesRequired); + uint cost = temp.div(decimals); + return (balanceDownBy, votesRequired, cost); + } + + /** + * @dev Used internally in order to get a dapp while checking if it exists + * @return existing dapp + */ + function _getDAppById(bytes32 _id) internal view returns(Data storage d) { + uint dappIdx = id2index[_id]; + Data memory d = dapps[dappIdx]; + require(d.id == _id, "Error fetching correct data"); + + return dapps[dappIdx]; + } + + /** + * @dev Decodes abi encoded data with selector for "functionName(bytes32,uint256)". + * @param _data Abi encoded data. + * @return Decoded registry call. + */ + function abiDecodeRegister( + bytes memory _data + ) + private + returns( + bytes4 sig, + bytes32 id, + uint256 amount, + bytes32 metadata + ) + { + assembly { + sig := mload(add(_data, add(0x20, 0))) + id := mload(add(_data, 36)) + amount := mload(add(_data, 68)) + metadata := mload(add(_data, 100)) + } + } +} \ No newline at end of file diff --git a/contracts/common/Controlled.sol b/contracts/common/Controlled.sol new file mode 100644 index 0000000..b7fd63d --- /dev/null +++ b/contracts/common/Controlled.sol @@ -0,0 +1,23 @@ +pragma solidity ^0.5.2; + + +contract Controlled { + /// @notice The address of the controller is the only address that can call + /// a function with this modifier + modifier onlyController { + require(msg.sender == controller, "Unauthorized"); + _; + } + + address payable public controller; + + constructor() internal { + controller = msg.sender; + } + + /// @notice Changes the controller of the contract + /// @param _newController The new controller of the contract + function changeController(address payable _newController) external onlyController { + controller = _newController; + } +} \ No newline at end of file diff --git a/contracts/test/TestBancorFormula.sol b/contracts/test/TestBancorFormula.sol new file mode 100644 index 0000000..3b66e07 --- /dev/null +++ b/contracts/test/TestBancorFormula.sol @@ -0,0 +1,44 @@ +pragma solidity ^0.5.2; +import "../utils/BancorFormula.sol"; + + +contract TestBancorFormula is BancorFormula { + + function powerTest( + uint256 _baseN, + uint256 _baseD, + uint32 _expN, + uint32 _expD) + external view returns (uint256, uint8) + { + return super.power( + _baseN, + _baseD, + _expN, + _expD); + } + + function generalLogTest(uint256 x) external pure returns (uint256) { + return super.generalLog(x); + } + + function floorLog2Test(uint256 _n) external pure returns (uint8) { + return super.floorLog2(_n); + } + + function findPositionInMaxExpArrayTest(uint256 _x) external view returns (uint8) { + return super.findPositionInMaxExpArray(_x); + } + + function generalExpTest(uint256 _x, uint8 _precision) external pure returns (uint256) { + return super.generalExp(_x, _precision); + } + + function optimalLogTest(uint256 x) external pure returns (uint256) { + return super.optimalLog(x); + } + + function optimalExpTest(uint256 x) external pure returns (uint256) { + return super.optimalExp(x); + } +} diff --git a/contracts/token/ApproveAndCallFallBack.sol b/contracts/token/ApproveAndCallFallBack.sol new file mode 100644 index 0000000..be51cf0 --- /dev/null +++ b/contracts/token/ApproveAndCallFallBack.sol @@ -0,0 +1,10 @@ +pragma solidity ^0.5.2; + + +contract ApproveAndCallFallBack { + function receiveApproval( + address from, + uint256 _amount, + address _token, + bytes calldata _data) external; +} diff --git a/contracts/token/ERC20Token.sol b/contracts/token/ERC20Token.sol new file mode 100644 index 0000000..5f8c11f --- /dev/null +++ b/contracts/token/ERC20Token.sol @@ -0,0 +1,53 @@ +pragma solidity ^0.5.2; + +// Abstract contract for the full ERC 20 Token standard +// https://github.com/ethereum/EIPs/issues/20 + +interface ERC20Token { + + /** + * @notice send `_value` token to `_to` from `msg.sender` + * @param _to The address of the recipient + * @param _value The amount of token to be transferred + * @return Whether the transfer was successful or not + */ + function transfer(address _to, uint256 _value) external returns (bool success); + + /** + * @notice `msg.sender` approves `_spender` to spend `_value` tokens + * @param _spender The address of the account able to transfer the tokens + * @param _value The amount of tokens to be approved for transfer + * @return Whether the approval was successful or not + */ + function approve(address _spender, uint256 _value) external returns (bool success); + + /** + * @notice send `_value` token to `_to` from `_from` on the condition it is approved by `_from` + * @param _from The address of the sender + * @param _to The address of the recipient + * @param _value The amount of token to be transferred + * @return Whether the transfer was successful or not + */ + function transferFrom(address _from, address _to, uint256 _value) external returns (bool success); + + /** + * @param _owner The address from which the balance will be retrieved + * @return The balance + */ + function balanceOf(address _owner) external view returns (uint256 balance); + + /** + * @param _owner The address of the account owning tokens + * @param _spender The address of the account able to transfer the tokens + * @return Amount of remaining tokens allowed to spent + */ + function allowance(address _owner, address _spender) external view returns (uint256 remaining); + + /** + * @notice return total supply of tokens + */ + function totalSupply() external view returns (uint256 supply); + + event Transfer(address indexed _from, address indexed _to, uint256 _value); + event Approval(address indexed _owner, address indexed _spender, uint256 _value); +} diff --git a/contracts/token/MiniMeToken.sol b/contracts/token/MiniMeToken.sol new file mode 100644 index 0000000..d2dbf21 --- /dev/null +++ b/contracts/token/MiniMeToken.sol @@ -0,0 +1,634 @@ +pragma solidity ^0.5.2; + +/* + Copyright 2016, Jordi Baylina + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ +/** + * @title MiniMeToken Contract + * @author Jordi Baylina + * @dev This token contract's goal is to make it easy for anyone to clone this + * token using the token distribution at a given block, this will allow DAO's + * and DApps to upgrade their features in a decentralized manner without + * affecting the original token + * @dev It is ERC20 compliant, but still needs to under go further testing. + */ + +import "../common/Controlled.sol"; +import "./TokenController.sol"; +import "./ApproveAndCallFallBack.sol"; +import "./MiniMeTokenInterface.sol"; +import "./TokenFactory.sol"; + +/** + * @dev The actual token contract, the default controller is the msg.sender + * that deploys the contract, so usually this token will be deployed by a + * token controller contract, which Giveth will call a "Campaign" + */ + + +contract MiniMeToken is MiniMeTokenInterface, Controlled { + + string public name; //The Token's name: e.g. DigixDAO Tokens + uint8 public decimals; //Number of decimals of the smallest unit + string public symbol; //An identifier: e.g. REP + string public constant VERSION = "MMT_0.1"; //An arbitrary versioning scheme + + /** + * @dev `Checkpoint` is the structure that attaches a block number to a + * given value, the block number attached is the one that last changed the + * value + */ + struct Checkpoint { + + // `fromBlock` is the block number that the value was generated from + uint128 fromBlock; + + // `value` is the amount of tokens at a specific block number + uint128 value; + } + + // `parentToken` is the Token address that was cloned to produce this token; + // it will be 0x0 for a token that was not cloned + MiniMeToken public parentToken; + + // `parentSnapShotBlock` is the block number from the Parent Token that was + // used to determine the initial distribution of the Clone Token + uint public parentSnapShotBlock; + + // `creationBlock` is the block number that the Clone Token was created + uint public creationBlock; + + // `balances` is the map that tracks the balance of each address, in this + // contract when the balance changes the block number that the change + // occurred is also included in the map + mapping (address => Checkpoint[]) balances; + + // `allowed` tracks any extra transfer rights as in all ERC20 tokens + mapping (address => mapping (address => uint256)) allowed; + + // Tracks the history of the `totalSupply` of the token + Checkpoint[] totalSupplyHistory; + + // Flag that determines if the token is transferable or not. + bool public transfersEnabled; + + // The factory used to create new clone tokens + TokenFactory public tokenFactory; + +//////////////// +// Constructor +//////////////// + + /** + * @notice Constructor to create a MiniMeToken + * @param _tokenFactory The address of the MiniMeTokenFactory contract that + * will create the Clone token contracts, the token factory needs to be + * deployed first + * @param _parentToken Address of the parent token, set to 0x0 if it is a + * new token + * @param _parentSnapShotBlock Block of the parent token that will + * determine the initial distribution of the clone token, set to 0 if it + * is a new token + * @param _tokenName Name of the new token + * @param _decimalUnits Number of decimals of the new token + * @param _tokenSymbol Token Symbol for the new token + * @param _transfersEnabled If true, tokens will be able to be transferred + */ + constructor( + address _tokenFactory, + address _parentToken, + uint _parentSnapShotBlock, + string memory _tokenName, + uint8 _decimalUnits, + string memory _tokenSymbol, + bool _transfersEnabled + ) + public + { + tokenFactory = TokenFactory(_tokenFactory); + name = _tokenName; // Set the name + decimals = _decimalUnits; // Set the decimals + symbol = _tokenSymbol; // Set the symbol + parentToken = MiniMeToken(address(uint160(_parentToken))); + parentSnapShotBlock = _parentSnapShotBlock; + transfersEnabled = _transfersEnabled; + creationBlock = block.number; + } + + +/////////////////// +// ERC20 Methods +/////////////////// + + /** + * @notice Send `_amount` tokens to `_to` from `msg.sender` + * @param _to The address of the recipient + * @param _amount The amount of tokens to be transferred + * @return Whether the transfer was successful or not + */ + function transfer(address _to, uint256 _amount) external returns (bool success) { + require(transfersEnabled); + return doTransfer(msg.sender, _to, _amount); + } + + /** + * @notice Send `_amount` tokens to `_to` from `_from` on the condition it + * is approved by `_from` + * @param _from The address holding the tokens being transferred + * @param _to The address of the recipient + * @param _amount The amount of tokens to be transferred + * @return True if the transfer was successful + */ + function transferFrom( + address _from, + address _to, + uint256 _amount + ) + external + returns (bool success) + { + + // The controller of this contract can move tokens around at will, + // this is important to recognize! Confirm that you trust the + // controller of this contract, which in most situations should be + // another open source smart contract or 0x0 + if (msg.sender != controller) { + require(transfersEnabled); + + // The standard ERC 20 transferFrom functionality + if (allowed[_from][msg.sender] < _amount) { + return false; + } + allowed[_from][msg.sender] -= _amount; + } + return doTransfer(_from, _to, _amount); + } + + /** + * @dev This is the actual transfer function in the token contract, it can + * only be called by other functions in this contract. + * @param _from The address holding the tokens being transferred + * @param _to The address of the recipient + * @param _amount The amount of tokens to be transferred + * @return True if the transfer was successful + */ + function doTransfer( + address _from, + address _to, + uint _amount + ) + internal + returns(bool) + { + + if (_amount == 0) { + return true; + } + + require(parentSnapShotBlock < block.number); + + // Do not allow transfer to 0x0 or the token contract itself + require((_to != address(0)) && (_to != address(this))); + + // If the amount being transfered is more than the balance of the + // account the transfer returns false + uint256 previousBalanceFrom = balanceOfAt(_from, block.number); + if (previousBalanceFrom < _amount) { + return false; + } + + // Alerts the token controller of the transfer + if (isContract(controller)) { + require(TokenController(controller).onTransfer(_from, _to, _amount)); + } + + // First update the balance array with the new value for the address + // sending the tokens + updateValueAtNow(balances[_from], previousBalanceFrom - _amount); + + // Then update the balance array with the new value for the address + // receiving the tokens + uint256 previousBalanceTo = balanceOfAt(_to, block.number); + require(previousBalanceTo + _amount >= previousBalanceTo); // Check for overflow + updateValueAtNow(balances[_to], previousBalanceTo + _amount); + + // An event to make the transfer easy to find on the blockchain + emit Transfer(_from, _to, _amount); + + return true; + } + + function doApprove( + address _from, + address _spender, + uint256 _amount + ) + internal + returns (bool) + { + require(transfersEnabled); + + // To change the approve amount you first have to reduce the addresses` + // allowance to zero by calling `approve(_spender,0)` if it is not + // already 0 to mitigate the race condition described here: + // https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 + require((_amount == 0) || (allowed[_from][_spender] == 0)); + + // Alerts the token controller of the approve function call + if (isContract(controller)) { + require(TokenController(controller).onApprove(_from, _spender, _amount)); + } + + allowed[_from][_spender] = _amount; + emit Approval(_from, _spender, _amount); + return true; + } + + /** + * @param _owner The address that's balance is being requested + * @return The balance of `_owner` at the current block + */ + function balanceOf(address _owner) external view returns (uint256 balance) { + return balanceOfAt(_owner, block.number); + } + + /** + * @notice `msg.sender` approves `_spender` to spend `_amount` tokens on + * its behalf. This is a modified version of the ERC20 approve function + * to be a little bit safer + * @param _spender The address of the account able to transfer the tokens + * @param _amount The amount of tokens to be approved for transfer + * @return True if the approval was successful + */ + function approve(address _spender, uint256 _amount) external returns (bool success) { + doApprove(msg.sender, _spender, _amount); + } + + /** + * @dev This function makes it easy to read the `allowed[]` map + * @param _owner The address of the account that owns the token + * @param _spender The address of the account able to transfer the tokens + * @return Amount of remaining tokens of _owner that _spender is allowed + * to spend + */ + function allowance( + address _owner, + address _spender + ) + external + view + returns (uint256 remaining) + { + return allowed[_owner][_spender]; + } + /** + * @notice `msg.sender` approves `_spender` to send `_amount` tokens on + * its behalf, and then a function is triggered in the contract that is + * being approved, `_spender`. This allows users to use their tokens to + * interact with contracts in one function call instead of two + * @param _spender The address of the contract able to transfer the tokens + * @param _amount The amount of tokens to be approved for transfer + * @return True if the function call was successful + */ + function approveAndCall( + address _spender, + uint256 _amount, + bytes calldata _extraData + ) + external + returns (bool success) + { + require(doApprove(msg.sender, _spender, _amount)); + + ApproveAndCallFallBack(_spender).receiveApproval( + msg.sender, + _amount, + address(this), + _extraData + ); + + return true; + } + + /** + * @dev This function makes it easy to get the total number of tokens + * @return The total number of tokens + */ + function totalSupply() external view returns (uint) { + return totalSupplyAt(block.number); + } + + +//////////////// +// Query balance and totalSupply in History +//////////////// + + /** + * @dev Queries the balance of `_owner` at a specific `_blockNumber` + * @param _owner The address from which the balance will be retrieved + * @param _blockNumber The block number when the balance is queried + * @return The balance at `_blockNumber` + */ + function balanceOfAt( + address _owner, + uint _blockNumber + ) + public + view + returns (uint) + { + + // These next few lines are used when the balance of the token is + // requested before a check point was ever created for this token, it + // requires that the `parentToken.balanceOfAt` be queried at the + // genesis block for that token as this contains initial balance of + // this token + if ((balances[_owner].length == 0) || (balances[_owner][0].fromBlock > _blockNumber)) { + if (address(parentToken) != address(0)) { + return parentToken.balanceOfAt(_owner, min(_blockNumber, parentSnapShotBlock)); + } else { + // Has no parent + return 0; + } + + // This will return the expected balance during normal situations + } else { + return getValueAt(balances[_owner], _blockNumber); + } + } + + /** + * @notice Total amount of tokens at a specific `_blockNumber`. + * @param _blockNumber The block number when the totalSupply is queried + * @return The total amount of tokens at `_blockNumber` + */ + function totalSupplyAt(uint _blockNumber) public view returns(uint) { + + // These next few lines are used when the totalSupply of the token is + // requested before a check point was ever created for this token, it + // requires that the `parentToken.totalSupplyAt` be queried at the + // genesis block for this token as that contains totalSupply of this + // token at this block number. + if ((totalSupplyHistory.length == 0) || (totalSupplyHistory[0].fromBlock > _blockNumber)) { + if (address(parentToken) != address(0)) { + return parentToken.totalSupplyAt(min(_blockNumber, parentSnapShotBlock)); + } else { + return 0; + } + + // This will return the expected totalSupply during normal situations + } else { + return getValueAt(totalSupplyHistory, _blockNumber); + } + } + +//////////////// +// Clone Token Method +//////////////// + + /** + * @notice Creates a new clone token with the initial distribution being + * this token at `snapshotBlock` + * @param _cloneTokenName Name of the clone token + * @param _cloneDecimalUnits Number of decimals of the smallest unit + * @param _cloneTokenSymbol Symbol of the clone token + * @param _snapshotBlock Block when the distribution of the parent token is + * copied to set the initial distribution of the new clone token; + * if the block is zero than the actual block, the current block is used + * @param _transfersEnabled True if transfers are allowed in the clone + * @return The address of the new MiniMeToken Contract + */ + function createCloneToken( + string calldata _cloneTokenName, + uint8 _cloneDecimalUnits, + string calldata _cloneTokenSymbol, + uint _snapshotBlock, + bool _transfersEnabled + ) + external + returns(address) + { + uint snapshotBlock = _snapshotBlock; + if (snapshotBlock == 0) { + snapshotBlock = block.number; + } + MiniMeToken cloneToken = MiniMeToken( + tokenFactory.createCloneToken( + address(this), + snapshotBlock, + _cloneTokenName, + _cloneDecimalUnits, + _cloneTokenSymbol, + _transfersEnabled + )); + + cloneToken.changeController(msg.sender); + + // An event to make the token easy to find on the blockchain + emit NewCloneToken(address(cloneToken), snapshotBlock); + return address(cloneToken); + } + +//////////////// +// Generate and destroy tokens +//////////////// + + /** + * @notice Generates `_amount` tokens that are assigned to `_owner` + * @param _owner The address that will be assigned the new tokens + * @param _amount The quantity of tokens generated + * @return True if the tokens are generated correctly + */ + function generateTokens( + address _owner, + uint _amount + ) + external + onlyController + returns (bool) + { + uint curTotalSupply = totalSupplyAt(block.number); + require(curTotalSupply + _amount >= curTotalSupply); // Check for overflow + uint previousBalanceTo = balanceOfAt(_owner, block.number); + require(previousBalanceTo + _amount >= previousBalanceTo); // Check for overflow + updateValueAtNow(totalSupplyHistory, curTotalSupply + _amount); + updateValueAtNow(balances[_owner], previousBalanceTo + _amount); + emit Transfer(address(0), _owner, _amount); + return true; + } + + /** + * @notice Burns `_amount` tokens from `_owner` + * @param _owner The address that will lose the tokens + * @param _amount The quantity of tokens to burn + * @return True if the tokens are burned correctly + */ + function destroyTokens( + address _owner, + uint _amount + ) + external + onlyController + returns (bool) + { + uint curTotalSupply = totalSupplyAt(block.number); + require(curTotalSupply >= _amount); + uint previousBalanceFrom = balanceOfAt(_owner, block.number); + require(previousBalanceFrom >= _amount); + updateValueAtNow(totalSupplyHistory, curTotalSupply - _amount); + updateValueAtNow(balances[_owner], previousBalanceFrom - _amount); + emit Transfer(_owner, address(0), _amount); + return true; + } + +//////////////// +// Enable tokens transfers +//////////////// + + /** + * @notice Enables token holders to transfer their tokens freely if true + * @param _transfersEnabled True if transfers are allowed in the clone + */ + function enableTransfers(bool _transfersEnabled) external onlyController { + transfersEnabled = _transfersEnabled; + } + +//////////////// +// Internal helper functions to query and set a value in a snapshot array +//////////////// + + /** + * @dev `getValueAt` retrieves the number of tokens at a given block number + * @param checkpoints The history of values being queried + * @param _block The block number to retrieve the value at + * @return The number of tokens being queried + */ + function getValueAt( + Checkpoint[] storage checkpoints, + uint _block + ) + internal + view + returns (uint) + { + if (checkpoints.length == 0) { + return 0; + } + + // Shortcut for the actual value + if (_block >= checkpoints[checkpoints.length-1].fromBlock) { + return checkpoints[checkpoints.length-1].value; + } + if (_block < checkpoints[0].fromBlock) { + return 0; + } + + // Binary search of the value in the array + uint min = 0; + uint max = checkpoints.length-1; + while (max > min) { + uint mid = (max + min + 1) / 2; + if (checkpoints[mid].fromBlock<=_block) { + min = mid; + } else { + max = mid-1; + } + } + return checkpoints[min].value; + } + + /** + * @dev `updateValueAtNow` used to update the `balances` map and the + * `totalSupplyHistory` + * @param checkpoints The history of data being updated + * @param _value The new number of tokens + */ + function updateValueAtNow(Checkpoint[] storage checkpoints, uint _value) internal { + if ((checkpoints.length == 0) || (checkpoints[checkpoints.length - 1].fromBlock < block.number)) { + Checkpoint storage newCheckPoint = checkpoints[checkpoints.length++]; + newCheckPoint.fromBlock = uint128(block.number); + newCheckPoint.value = uint128(_value); + } else { + Checkpoint storage oldCheckPoint = checkpoints[checkpoints.length-1]; + oldCheckPoint.value = uint128(_value); + } + } + + /** + * @dev Internal function to determine if an address is a contract + * @param _addr The address being queried + * @return True if `_addr` is a contract + */ + function isContract(address _addr) internal returns(bool) { + uint size; + if (_addr == address(0)) { + return false; + } + assembly { + size := extcodesize(_addr) + } + return size>0; + } + + /** + * @dev Helper function to return a min betwen the two uints + */ + function min(uint a, uint b) internal pure returns (uint) { + return a < b ? a : b; + } + + /** + * @notice The fallback function: If the contract's controller has not been + * set to 0, then the `proxyPayment` method is called which relays the + * ether and creates tokens as described in the token controller contract + */ + function () external payable { + require(isContract(controller)); + require(TokenController(controller).proxyPayment.value(msg.value)(msg.sender)); + } + +////////// +// Safety Methods +////////// + + /** + * @notice This method can be used by the controller to extract mistakenly + * sent tokens to this contract. + * @param _token The address of the token contract that you want to recover + * set to 0 in case you want to extract ether. + */ + function claimTokens(address _token) external onlyController { + if (_token == address(0)) { + controller.transfer(address(this).balance); + return; + } + + MiniMeToken token = MiniMeToken(address(uint160(_token))); + uint balance = token.balanceOf(address(this)); + token.transfer(controller, balance); + emit ClaimedTokens(_token, controller, balance); + } + +//////////////// +// Events +//////////////// + event ClaimedTokens(address indexed _token, address indexed _controller, uint _amount); + event Transfer(address indexed _from, address indexed _to, uint256 _amount); + event NewCloneToken(address indexed _cloneToken, uint snapshotBlock); + event Approval( + address indexed _owner, + address indexed _spender, + uint256 _amount + ); + +} \ No newline at end of file diff --git a/contracts/token/MiniMeTokenFactory.sol b/contracts/token/MiniMeTokenFactory.sol new file mode 100644 index 0000000..cb63393 --- /dev/null +++ b/contracts/token/MiniMeTokenFactory.sol @@ -0,0 +1,48 @@ +pragma solidity ^0.5.2; + +import "./TokenFactory.sol"; +import "./MiniMeToken.sol"; + + +/** + * @dev This contract is used to generate clone contracts from a contract. + * In solidity this is the way to create a contract from a contract of the + * same class + */ +contract MiniMeTokenFactory is TokenFactory { + + /** + * @notice Update the DApp by creating a new token with new functionalities + * the msg.sender becomes the controller of this clone token + * @param _parentToken Address of the token being cloned + * @param _snapshotBlock Block of the parent token that will + * determine the initial distribution of the clone token + * @param _tokenName Name of the new token + * @param _decimalUnits Number of decimals of the new token + * @param _tokenSymbol Token Symbol for the new token + * @param _transfersEnabled If true, tokens will be able to be transferred + * @return The address of the new token contract + */ + function createCloneToken( + address _parentToken, + uint _snapshotBlock, + string calldata _tokenName, + uint8 _decimalUnits, + string calldata _tokenSymbol, + bool _transfersEnabled + ) external returns (address payable) + { + MiniMeToken newToken = new MiniMeToken( + address(this), + _parentToken, + _snapshotBlock, + _tokenName, + _decimalUnits, + _tokenSymbol, + _transfersEnabled + ); + + newToken.changeController(msg.sender); + return address(newToken); + } +} \ No newline at end of file diff --git a/contracts/token/MiniMeTokenInterface.sol b/contracts/token/MiniMeTokenInterface.sol new file mode 100644 index 0000000..0b2256b --- /dev/null +++ b/contracts/token/MiniMeTokenInterface.sol @@ -0,0 +1,108 @@ +pragma solidity ^0.5.2; + +import "./ERC20Token.sol"; + + +contract MiniMeTokenInterface is ERC20Token { + + /** + * @notice `msg.sender` approves `_spender` to send `_amount` tokens on + * its behalf, and then a function is triggered in the contract that is + * being approved, `_spender`. This allows users to use their tokens to + * interact with contracts in one function call instead of two + * @param _spender The address of the contract able to transfer the tokens + * @param _amount The amount of tokens to be approved for transfer + * @return True if the function call was successful + */ + function approveAndCall( + address _spender, + uint256 _amount, + bytes calldata _extraData + ) + external + returns (bool success); + + /** + * @notice Creates a new clone token with the initial distribution being + * this token at `_snapshotBlock` + * @param _cloneTokenName Name of the clone token + * @param _cloneDecimalUnits Number of decimals of the smallest unit + * @param _cloneTokenSymbol Symbol of the clone token + * @param _snapshotBlock Block when the distribution of the parent token is + * copied to set the initial distribution of the new clone token; + * if the block is zero than the actual block, the current block is used + * @param _transfersEnabled True if transfers are allowed in the clone + * @return The address of the new MiniMeToken Contract + */ + function createCloneToken( + string calldata _cloneTokenName, + uint8 _cloneDecimalUnits, + string calldata _cloneTokenSymbol, + uint _snapshotBlock, + bool _transfersEnabled + ) + external + returns(address); + + /** + * @notice Generates `_amount` tokens that are assigned to `_owner` + * @param _owner The address that will be assigned the new tokens + * @param _amount The quantity of tokens generated + * @return True if the tokens are generated correctly + */ + function generateTokens( + address _owner, + uint _amount + ) + external + returns (bool); + + /** + * @notice Burns `_amount` tokens from `_owner` + * @param _owner The address that will lose the tokens + * @param _amount The quantity of tokens to burn + * @return True if the tokens are burned correctly + */ + function destroyTokens( + address _owner, + uint _amount + ) + external + returns (bool); + + /** + * @notice Enables token holders to transfer their tokens freely if true + * @param _transfersEnabled True if transfers are allowed in the clone + */ + function enableTransfers(bool _transfersEnabled) external; + + /** + * @notice This method can be used by the controller to extract mistakenly + * sent tokens to this contract. + * @param _token The address of the token contract that you want to recover + * set to 0 in case you want to extract ether. + */ + function claimTokens(address _token) external; + + /** + * @dev Queries the balance of `_owner` at a specific `_blockNumber` + * @param _owner The address from which the balance will be retrieved + * @param _blockNumber The block number when the balance is queried + * @return The balance at `_blockNumber` + */ + function balanceOfAt( + address _owner, + uint _blockNumber + ) + public + view + returns (uint); + + /** + * @notice Total amount of tokens at a specific `_blockNumber`. + * @param _blockNumber The block number when the totalSupply is queried + * @return The total amount of tokens at `_blockNumber` + */ + function totalSupplyAt(uint _blockNumber) public view returns(uint); + +} \ No newline at end of file diff --git a/contracts/token/TokenController.sol b/contracts/token/TokenController.sol new file mode 100644 index 0000000..ae55d58 --- /dev/null +++ b/contracts/token/TokenController.sol @@ -0,0 +1,35 @@ +pragma solidity ^0.5.2; + + +/** + * @dev The token controller contract must implement these functions + */ +interface TokenController { + /** + * @notice Called when `_owner` sends ether to the MiniMe Token contract + * @param _owner The address that sent the ether to create tokens + * @return True if the ether is accepted, false if it throws + */ + function proxyPayment(address _owner) external payable returns(bool); + + /** + * @notice Notifies the controller about a token transfer allowing the + * controller to react if desired + * @param _from The origin of the transfer + * @param _to The destination of the transfer + * @param _amount The amount of the transfer + * @return False if the controller does not authorize the transfer + */ + function onTransfer(address _from, address _to, uint _amount) external returns(bool); + + /** + * @notice Notifies the controller about an approval allowing the + * controller to react if desired + * @param _owner The address that calls `approve()` + * @param _spender The spender in the `approve()` call + * @param _amount The amount in the `approve()` call + * @return False if the controller does not authorize the approval + */ + function onApprove(address _owner, address _spender, uint _amount) external + returns(bool); +} \ No newline at end of file diff --git a/contracts/token/TokenFactory.sol b/contracts/token/TokenFactory.sol new file mode 100644 index 0000000..de62eef --- /dev/null +++ b/contracts/token/TokenFactory.sol @@ -0,0 +1,13 @@ +pragma solidity ^0.5.2; + + +contract TokenFactory { + function createCloneToken( + address _parentToken, + uint _snapshotBlock, + string calldata _tokenName, + uint8 _decimalUnits, + string calldata _tokenSymbol, + bool _transfersEnabled + ) external returns (address payable); +} diff --git a/contracts/utils/BancorFormula.sol b/contracts/utils/BancorFormula.sol new file mode 100644 index 0000000..c93450a --- /dev/null +++ b/contracts/utils/BancorFormula.sol @@ -0,0 +1,506 @@ +pragma solidity ^0.5.2; +import "./SafeMath.sol"; + + +contract BancorFormula { + using SafeMath for uint256; + + uint256 private constant ONE = 1; + uint8 private constant MIN_PRECISION = 32; + uint8 private constant MAX_PRECISION = 127; + + /** + Auto-generated via 'PrintIntScalingFactors.py' + */ + uint256 private constant FIXED_1 = 0x080000000000000000000000000000000; + uint256 private constant FIXED_2 = 0x100000000000000000000000000000000; + uint256 private constant MAX_NUM = 0x200000000000000000000000000000000; + + /** + Auto-generated via 'PrintLn2ScalingFactors.py' + */ + uint256 private constant LN2_NUMERATOR = 0x3f80fe03f80fe03f80fe03f80fe03f8; + uint256 private constant LN2_DENOMINATOR = 0x5b9de1d10bf4103d647b0955897ba80; + + /** + Auto-generated via 'PrintFunctionOptimalLog.py' and 'PrintFunctionOptimalExp.py' + */ + uint256 private constant OPT_LOG_MAX_VAL = 0x15bf0a8b1457695355fb8ac404e7a79e3; + uint256 private constant OPT_EXP_MAX_VAL = 0x800000000000000000000000000000000; + + /** + Auto-generated via 'PrintFunctionConstructor.py' + */ + uint256[128] private maxExpArray; + constructor() public { + // maxExpArray[0] = 0x6bffffffffffffffffffffffffffffffff; + // maxExpArray[1] = 0x67ffffffffffffffffffffffffffffffff; + // maxExpArray[2] = 0x637fffffffffffffffffffffffffffffff; + // maxExpArray[3] = 0x5f6fffffffffffffffffffffffffffffff; + // maxExpArray[4] = 0x5b77ffffffffffffffffffffffffffffff; + // maxExpArray[5] = 0x57b3ffffffffffffffffffffffffffffff; + // maxExpArray[6] = 0x5419ffffffffffffffffffffffffffffff; + // maxExpArray[7] = 0x50a2ffffffffffffffffffffffffffffff; + // maxExpArray[8] = 0x4d517fffffffffffffffffffffffffffff; + // maxExpArray[9] = 0x4a233fffffffffffffffffffffffffffff; + // maxExpArray[10] = 0x47165fffffffffffffffffffffffffffff; + // maxExpArray[11] = 0x4429afffffffffffffffffffffffffffff; + // maxExpArray[12] = 0x415bc7ffffffffffffffffffffffffffff; + // maxExpArray[13] = 0x3eab73ffffffffffffffffffffffffffff; + // maxExpArray[14] = 0x3c1771ffffffffffffffffffffffffffff; + // maxExpArray[15] = 0x399e96ffffffffffffffffffffffffffff; + // maxExpArray[16] = 0x373fc47fffffffffffffffffffffffffff; + // maxExpArray[17] = 0x34f9e8ffffffffffffffffffffffffffff; + // maxExpArray[18] = 0x32cbfd5fffffffffffffffffffffffffff; + // maxExpArray[19] = 0x30b5057fffffffffffffffffffffffffff; + // maxExpArray[20] = 0x2eb40f9fffffffffffffffffffffffffff; + // maxExpArray[21] = 0x2cc8340fffffffffffffffffffffffffff; + // maxExpArray[22] = 0x2af09481ffffffffffffffffffffffffff; + // maxExpArray[23] = 0x292c5bddffffffffffffffffffffffffff; + // maxExpArray[24] = 0x277abdcdffffffffffffffffffffffffff; + // maxExpArray[25] = 0x25daf6657fffffffffffffffffffffffff; + // maxExpArray[26] = 0x244c49c65fffffffffffffffffffffffff; + // maxExpArray[27] = 0x22ce03cd5fffffffffffffffffffffffff; + // maxExpArray[28] = 0x215f77c047ffffffffffffffffffffffff; + // maxExpArray[29] = 0x1fffffffffffffffffffffffffffffffff; + // maxExpArray[30] = 0x1eaefdbdabffffffffffffffffffffffff; + // maxExpArray[31] = 0x1d6bd8b2ebffffffffffffffffffffffff; + maxExpArray[32] = 0x1c35fedd14ffffffffffffffffffffffff; + maxExpArray[33] = 0x1b0ce43b323fffffffffffffffffffffff; + maxExpArray[34] = 0x19f0028ec1ffffffffffffffffffffffff; + maxExpArray[35] = 0x18ded91f0e7fffffffffffffffffffffff; + maxExpArray[36] = 0x17d8ec7f0417ffffffffffffffffffffff; + maxExpArray[37] = 0x16ddc6556cdbffffffffffffffffffffff; + maxExpArray[38] = 0x15ecf52776a1ffffffffffffffffffffff; + maxExpArray[39] = 0x15060c256cb2ffffffffffffffffffffff; + maxExpArray[40] = 0x1428a2f98d72ffffffffffffffffffffff; + maxExpArray[41] = 0x13545598e5c23fffffffffffffffffffff; + maxExpArray[42] = 0x1288c4161ce1dfffffffffffffffffffff; + maxExpArray[43] = 0x11c592761c666fffffffffffffffffffff; + maxExpArray[44] = 0x110a688680a757ffffffffffffffffffff; + maxExpArray[45] = 0x1056f1b5bedf77ffffffffffffffffffff; + maxExpArray[46] = 0x0faadceceeff8bffffffffffffffffffff; + maxExpArray[47] = 0x0f05dc6b27edadffffffffffffffffffff; + maxExpArray[48] = 0x0e67a5a25da4107fffffffffffffffffff; + maxExpArray[49] = 0x0dcff115b14eedffffffffffffffffffff; + maxExpArray[50] = 0x0d3e7a392431239fffffffffffffffffff; + maxExpArray[51] = 0x0cb2ff529eb71e4fffffffffffffffffff; + maxExpArray[52] = 0x0c2d415c3db974afffffffffffffffffff; + maxExpArray[53] = 0x0bad03e7d883f69bffffffffffffffffff; + maxExpArray[54] = 0x0b320d03b2c343d5ffffffffffffffffff; + maxExpArray[55] = 0x0abc25204e02828dffffffffffffffffff; + maxExpArray[56] = 0x0a4b16f74ee4bb207fffffffffffffffff; + maxExpArray[57] = 0x09deaf736ac1f569ffffffffffffffffff; + maxExpArray[58] = 0x0976bd9952c7aa957fffffffffffffffff; + maxExpArray[59] = 0x09131271922eaa606fffffffffffffffff; + maxExpArray[60] = 0x08b380f3558668c46fffffffffffffffff; + maxExpArray[61] = 0x0857ddf0117efa215bffffffffffffffff; + maxExpArray[62] = 0x07ffffffffffffffffffffffffffffffff; + maxExpArray[63] = 0x07abbf6f6abb9d087fffffffffffffffff; + maxExpArray[64] = 0x075af62cbac95f7dfa7fffffffffffffff; + maxExpArray[65] = 0x070d7fb7452e187ac13fffffffffffffff; + maxExpArray[66] = 0x06c3390ecc8af379295fffffffffffffff; + maxExpArray[67] = 0x067c00a3b07ffc01fd6fffffffffffffff; + maxExpArray[68] = 0x0637b647c39cbb9d3d27ffffffffffffff; + maxExpArray[69] = 0x05f63b1fc104dbd39587ffffffffffffff; + maxExpArray[70] = 0x05b771955b36e12f7235ffffffffffffff; + maxExpArray[71] = 0x057b3d49dda84556d6f6ffffffffffffff; + maxExpArray[72] = 0x054183095b2c8ececf30ffffffffffffff; + maxExpArray[73] = 0x050a28be635ca2b888f77fffffffffffff; + maxExpArray[74] = 0x04d5156639708c9db33c3fffffffffffff; + maxExpArray[75] = 0x04a23105873875bd52dfdfffffffffffff; + maxExpArray[76] = 0x0471649d87199aa990756fffffffffffff; + maxExpArray[77] = 0x04429a21a029d4c1457cfbffffffffffff; + maxExpArray[78] = 0x0415bc6d6fb7dd71af2cb3ffffffffffff; + maxExpArray[79] = 0x03eab73b3bbfe282243ce1ffffffffffff; + maxExpArray[80] = 0x03c1771ac9fb6b4c18e229ffffffffffff; + maxExpArray[81] = 0x0399e96897690418f785257fffffffffff; + maxExpArray[82] = 0x0373fc456c53bb779bf0ea9fffffffffff; + maxExpArray[83] = 0x034f9e8e490c48e67e6ab8bfffffffffff; + maxExpArray[84] = 0x032cbfd4a7adc790560b3337ffffffffff; + maxExpArray[85] = 0x030b50570f6e5d2acca94613ffffffffff; + maxExpArray[86] = 0x02eb40f9f620fda6b56c2861ffffffffff; + maxExpArray[87] = 0x02cc8340ecb0d0f520a6af58ffffffffff; + maxExpArray[88] = 0x02af09481380a0a35cf1ba02ffffffffff; + maxExpArray[89] = 0x0292c5bdd3b92ec810287b1b3fffffffff; + maxExpArray[90] = 0x0277abdcdab07d5a77ac6d6b9fffffffff; + maxExpArray[91] = 0x025daf6654b1eaa55fd64df5efffffffff; + maxExpArray[92] = 0x0244c49c648baa98192dce88b7ffffffff; + maxExpArray[93] = 0x022ce03cd5619a311b2471268bffffffff; + maxExpArray[94] = 0x0215f77c045fbe885654a44a0fffffffff; + maxExpArray[95] = 0x01ffffffffffffffffffffffffffffffff; + maxExpArray[96] = 0x01eaefdbdaaee7421fc4d3ede5ffffffff; + maxExpArray[97] = 0x01d6bd8b2eb257df7e8ca57b09bfffffff; + maxExpArray[98] = 0x01c35fedd14b861eb0443f7f133fffffff; + maxExpArray[99] = 0x01b0ce43b322bcde4a56e8ada5afffffff; + maxExpArray[100] = 0x019f0028ec1fff007f5a195a39dfffffff; + maxExpArray[101] = 0x018ded91f0e72ee74f49b15ba527ffffff; + maxExpArray[102] = 0x017d8ec7f04136f4e5615fd41a63ffffff; + maxExpArray[103] = 0x016ddc6556cdb84bdc8d12d22e6fffffff; + maxExpArray[104] = 0x015ecf52776a1155b5bd8395814f7fffff; + maxExpArray[105] = 0x015060c256cb23b3b3cc3754cf40ffffff; + maxExpArray[106] = 0x01428a2f98d728ae223ddab715be3fffff; + maxExpArray[107] = 0x013545598e5c23276ccf0ede68034fffff; + maxExpArray[108] = 0x01288c4161ce1d6f54b7f61081194fffff; + maxExpArray[109] = 0x011c592761c666aa641d5a01a40f17ffff; + maxExpArray[110] = 0x0110a688680a7530515f3e6e6cfdcdffff; + maxExpArray[111] = 0x01056f1b5bedf75c6bcb2ce8aed428ffff; + maxExpArray[112] = 0x00faadceceeff8a0890f3875f008277fff; + maxExpArray[113] = 0x00f05dc6b27edad306388a600f6ba0bfff; + maxExpArray[114] = 0x00e67a5a25da41063de1495d5b18cdbfff; + maxExpArray[115] = 0x00dcff115b14eedde6fc3aa5353f2e4fff; + maxExpArray[116] = 0x00d3e7a3924312399f9aae2e0f868f8fff; + maxExpArray[117] = 0x00cb2ff529eb71e41582cccd5a1ee26fff; + maxExpArray[118] = 0x00c2d415c3db974ab32a51840c0b67edff; + maxExpArray[119] = 0x00bad03e7d883f69ad5b0a186184e06bff; + maxExpArray[120] = 0x00b320d03b2c343d4829abd6075f0cc5ff; + maxExpArray[121] = 0x00abc25204e02828d73c6e80bcdb1a95bf; + maxExpArray[122] = 0x00a4b16f74ee4bb2040a1ec6c15fbbf2df; + maxExpArray[123] = 0x009deaf736ac1f569deb1b5ae3f36c130f; + maxExpArray[124] = 0x00976bd9952c7aa957f5937d790ef65037; + maxExpArray[125] = 0x009131271922eaa6064b73a22d0bd4f2bf; + maxExpArray[126] = 0x008b380f3558668c46c91c49a2f8e967b9; + maxExpArray[127] = 0x00857ddf0117efa215952912839f6473e6; + } + + /** + General Description: + Determine a value of precision. + Calculate an integer approximation of (_baseN / _baseD) ^ (_expN / _expD) * 2 ^ precision. + Return the result along with the precision used. + Detailed Description: + Instead of calculating "base ^ exp", we calculate "e ^ (log(base) * exp)". + The value of "log(base)" is represented with an integer slightly smaller than "log(base) * 2 ^ precision". + The larger "precision" is, the more accurately this value represents the real value. + However, the larger "precision" is, the more bits are required in order to store this value. + And the exponentiation function, which takes "x" and calculates "e ^ x", is limited to a maximum exponent (maximum value of "x"). + This maximum exponent depends on the "precision" used, and it is given by "maxExpArray[precision] >> (MAX_PRECISION - precision)". + Hence we need to determine the highest precision which can be used for the given input, before calling the exponentiation function. + This allows us to compute "base ^ exp" with maximum accuracy and without exceeding 256 bits in any of the intermediate computations. + This functions assumes that "_expN < 2 ^ 256 / log(MAX_NUM - 1)", otherwise the multiplication should be replaced with a "safeMul". + */ + function power( + uint256 _baseN, + uint256 _baseD, + uint32 _expN, + uint32 _expD) internal view returns (uint256, uint8) + { + require(_baseN < MAX_NUM, "SNT available is invalid"); + + uint256 baseLog; + uint256 base = _baseN * FIXED_1 / _baseD; + if (base < OPT_LOG_MAX_VAL) { + baseLog = optimalLog(base); + } else { + baseLog = generalLog(base); + } + + uint256 baseLogTimesExp = baseLog * _expN / _expD; + if (baseLogTimesExp < OPT_EXP_MAX_VAL) { + return (optimalExp(baseLogTimesExp), MAX_PRECISION); + } else { + uint8 precision = findPositionInMaxExpArray(baseLogTimesExp); + return (generalExp(baseLogTimesExp >> (MAX_PRECISION - precision), precision), precision); + } + } + + /** + Compute log(x / FIXED_1) * FIXED_1. + This functions assumes that "x >= FIXED_1", because the output would be negative otherwise. + */ + function generalLog(uint256 x) internal pure returns (uint256) { + uint256 res = 0; + + // If x >= 2, then we compute the integer part of log2(x), which is larger than 0. + if (x >= FIXED_2) { + uint8 count = floorLog2(x / FIXED_1); + x >>= count; // now x < 2 + res = count * FIXED_1; + } + + // If x > 1, then we compute the fraction part of log2(x), which is larger than 0. + if (x > FIXED_1) { + for (uint8 i = MAX_PRECISION; i > 0; --i) { + x = (x * x) / FIXED_1; // now 1 < x < 4 + if (x >= FIXED_2) { + x >>= 1; // now 1 < x < 2 + res += ONE << (i - 1); + } + } + } + + return res * LN2_NUMERATOR / LN2_DENOMINATOR; + } + + /** + Compute the largest integer smaller than or equal to the binary logarithm of the input. + */ + function floorLog2(uint256 _n) internal pure returns (uint8) { + uint8 res = 0; + + if (_n < 256) { + // At most 8 iterations + while (_n > 1) { + _n >>= 1; + res += 1; + } + } else { + // Exactly 8 iterations + for (uint8 s = 128; s > 0; s >>= 1) { + if (_n >= (ONE << s)) { + _n >>= s; + res |= s; + } + } + } + + return res; + } + + /** + The global "maxExpArray" is sorted in descending order, and therefore the following statements are equivalent: + - This function finds the position of [the smallest value in "maxExpArray" larger than or equal to "x"] + - This function finds the highest position of [a value in "maxExpArray" larger than or equal to "x"] + */ + function findPositionInMaxExpArray(uint256 _x) internal view returns (uint8) { + uint8 lo = MIN_PRECISION; + uint8 hi = MAX_PRECISION; + + while (lo + 1 < hi) { + uint8 mid = (lo + hi) / 2; + if (maxExpArray[mid] >= _x) { + lo = mid; + } else { + hi = mid; + } + } + + if (maxExpArray[hi] >= _x) + return hi; + if (maxExpArray[lo] >= _x) + return lo; + + require(false, "Could not find a suitable position"); + return 0; + } + + /** + This function can be auto-generated by the script 'PrintFunctionGeneralExp.py'. + It approximates "e ^ x" via maclaurin summation: "(x^0)/0! + (x^1)/1! + ... + (x^n)/n!". + It returns "e ^ (x / 2 ^ precision) * 2 ^ precision", that is, the result is upshifted for accuracy. + The global "maxExpArray" maps each "precision" to "((maximumExponent + 1) << (MAX_PRECISION - precision)) - 1". + The maximum permitted value for "x" is therefore given by "maxExpArray[precision] >> (MAX_PRECISION - precision)". + */ + function generalExp(uint256 _x, uint8 _precision) internal pure returns (uint256) { + uint256 xi = _x; + uint256 res = 0; + + xi = (xi * _x) >> _precision; + res += xi * 0x3442c4e6074a82f1797f72ac0000000; // add x^02 * (33! / 02!) + xi = (xi * _x) >> _precision; + res += xi * 0x116b96f757c380fb287fd0e40000000; // add x^03 * (33! / 03!) + xi = (xi * _x) >> _precision; + res += xi * 0x045ae5bdd5f0e03eca1ff4390000000; // add x^04 * (33! / 04!) + xi = (xi * _x) >> _precision; + res += xi * 0x00defabf91302cd95b9ffda50000000; // add x^05 * (33! / 05!) + xi = (xi * _x) >> _precision; + res += xi * 0x002529ca9832b22439efff9b8000000; // add x^06 * (33! / 06!) + xi = (xi * _x) >> _precision; + res += xi * 0x00054f1cf12bd04e516b6da88000000; // add x^07 * (33! / 07!) + xi = (xi * _x) >> _precision; + res += xi * 0x0000a9e39e257a09ca2d6db51000000; // add x^08 * (33! / 08!) + xi = (xi * _x) >> _precision; + res += xi * 0x000012e066e7b839fa050c309000000; // add x^09 * (33! / 09!) + xi = (xi * _x) >> _precision; + res += xi * 0x000001e33d7d926c329a1ad1a800000; // add x^10 * (33! / 10!) + xi = (xi * _x) >> _precision; + res += xi * 0x0000002bee513bdb4a6b19b5f800000; // add x^11 * (33! / 11!) + xi = (xi * _x) >> _precision; + res += xi * 0x00000003a9316fa79b88eccf2a00000; // add x^12 * (33! / 12!) + xi = (xi * _x) >> _precision; + res += xi * 0x0000000048177ebe1fa812375200000; // add x^13 * (33! / 13!) + xi = (xi * _x) >> _precision; + res += xi * 0x0000000005263fe90242dcbacf00000; // add x^14 * (33! / 14!) + xi = (xi * _x) >> _precision; + res += xi * 0x000000000057e22099c030d94100000; // add x^15 * (33! / 15!) + xi = (xi * _x) >> _precision; + res += xi * 0x0000000000057e22099c030d9410000; // add x^16 * (33! / 16!) + xi = (xi * _x) >> _precision; + res += xi * 0x00000000000052b6b54569976310000; // add x^17 * (33! / 17!) + xi = (xi * _x) >> _precision; + res += xi * 0x00000000000004985f67696bf748000; // add x^18 * (33! / 18!) + xi = (xi * _x) >> _precision; + res += xi * 0x000000000000003dea12ea99e498000; // add x^19 * (33! / 19!) + xi = (xi * _x) >> _precision; + res += xi * 0x00000000000000031880f2214b6e000; // add x^20 * (33! / 20!) + xi = (xi * _x) >> _precision; + res += xi * 0x000000000000000025bcff56eb36000; // add x^21 * (33! / 21!) + xi = (xi * _x) >> _precision; + res += xi * 0x000000000000000001b722e10ab1000; // add x^22 * (33! / 22!) + xi = (xi * _x) >> _precision; + res += xi * 0x0000000000000000001317c70077000; // add x^23 * (33! / 23!) + xi = (xi * _x) >> _precision; + res += xi * 0x00000000000000000000cba84aafa00; // add x^24 * (33! / 24!) + xi = (xi * _x) >> _precision; + res += xi * 0x00000000000000000000082573a0a00; // add x^25 * (33! / 25!) + xi = (xi * _x) >> _precision; + res += xi * 0x00000000000000000000005035ad900; // add x^26 * (33! / 26!) + xi = (xi * _x) >> _precision; + res += xi * 0x000000000000000000000002f881b00; // add x^27 * (33! / 27!) + xi = (xi * _x) >> _precision; + res += xi * 0x0000000000000000000000001b29340; // add x^28 * (33! / 28!) + xi = (xi * _x) >> _precision; + res += xi * 0x00000000000000000000000000efc40; // add x^29 * (33! / 29!) + xi = (xi * _x) >> _precision; + res += xi * 0x0000000000000000000000000007fe0; // add x^30 * (33! / 30!) + xi = (xi * _x) >> _precision; + res += xi * 0x0000000000000000000000000000420; // add x^31 * (33! / 31!) + xi = (xi * _x) >> _precision; + res += xi * 0x0000000000000000000000000000021; // add x^32 * (33! / 32!) + xi = (xi * _x) >> _precision; + res += xi * 0x0000000000000000000000000000001; // add x^33 * (33! / 33!) + + return res / 0x688589cc0e9505e2f2fee5580000000 + _x + (ONE << _precision); // divide by 33! and then add x^1 / 1! + x^0 / 0! + } + + /** + Return log(x / FIXED_1) * FIXED_1 + Input range: FIXED_1 <= x <= LOG_EXP_MAX_VAL - 1 + Auto-generated via 'PrintFunctionOptimalLog.py' + Detailed description: + - Rewrite the input as a product of natural exponents and a single residual r, such that 1 < r < 2 + - The natural logarithm of each (pre-calculated) exponent is the degree of the exponent + - The natural logarithm of r is calculated via Taylor series for log(1 + x), where x = r - 1 + - The natural logarithm of the input is calculated by summing up the intermediate results above + - For example: log(250) = log(e^4 * e^1 * e^0.5 * 1.021692859) = 4 + 1 + 0.5 + log(1 + 0.021692859) + */ + function optimalLog(uint256 x) internal pure returns (uint256) { + uint256 res = 0; + + uint256 y = 0; + uint256 z; + uint256 w; + + if (x >= 0xd3094c70f034de4b96ff7d5b6f99fcd8) { + res += 0x40000000000000000000000000000000; + x = x * FIXED_1 / 0xd3094c70f034de4b96ff7d5b6f99fcd8;} // add 1 / 2^1 + if (x >= 0xa45af1e1f40c333b3de1db4dd55f29a7) { + res += 0x20000000000000000000000000000000; + x = x * FIXED_1 / 0xa45af1e1f40c333b3de1db4dd55f29a7;} // add 1 / 2^2 + if (x >= 0x910b022db7ae67ce76b441c27035c6a1) { + res += 0x10000000000000000000000000000000; + x = x * FIXED_1 / 0x910b022db7ae67ce76b441c27035c6a1;} // add 1 / 2^3 + if (x >= 0x88415abbe9a76bead8d00cf112e4d4a8) { + res += 0x08000000000000000000000000000000; + x = x * FIXED_1 / 0x88415abbe9a76bead8d00cf112e4d4a8;} // add 1 / 2^4 + if (x >= 0x84102b00893f64c705e841d5d4064bd3) { + res += 0x04000000000000000000000000000000; + x = x * FIXED_1 / 0x84102b00893f64c705e841d5d4064bd3;} // add 1 / 2^5 + if (x >= 0x8204055aaef1c8bd5c3259f4822735a2) { + res += 0x02000000000000000000000000000000; + x = x * FIXED_1 / 0x8204055aaef1c8bd5c3259f4822735a2;} // add 1 / 2^6 + if (x >= 0x810100ab00222d861931c15e39b44e99) { + res += 0x01000000000000000000000000000000; + x = x * FIXED_1 / 0x810100ab00222d861931c15e39b44e99;} // add 1 / 2^7 + if (x >= 0x808040155aabbbe9451521693554f733) { + res += 0x00800000000000000000000000000000; + x = x * FIXED_1 / 0x808040155aabbbe9451521693554f733;} // add 1 / 2^8 + + z = y = x - FIXED_1; + w = y * y / FIXED_1; + res += z * (0x100000000000000000000000000000000 - y) / 0x100000000000000000000000000000000; + z = z * w / FIXED_1; // add y^01 / 01 - y^02 / 02 + res += z * (0x0aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa - y) / 0x200000000000000000000000000000000; + z = z * w / FIXED_1; // add y^03 / 03 - y^04 / 04 + res += z * (0x099999999999999999999999999999999 - y) / 0x300000000000000000000000000000000; + z = z * w / FIXED_1; // add y^05 / 05 - y^06 / 06 + res += z * (0x092492492492492492492492492492492 - y) / 0x400000000000000000000000000000000; + z = z * w / FIXED_1; // add y^07 / 07 - y^08 / 08 + res += z * (0x08e38e38e38e38e38e38e38e38e38e38e - y) / 0x500000000000000000000000000000000; + z = z * w / FIXED_1; // add y^09 / 09 - y^10 / 10 + res += z * (0x08ba2e8ba2e8ba2e8ba2e8ba2e8ba2e8b - y) / 0x600000000000000000000000000000000; + z = z * w / FIXED_1; // add y^11 / 11 - y^12 / 12 + res += z * (0x089d89d89d89d89d89d89d89d89d89d89 - y) / 0x700000000000000000000000000000000; + z = z * w / FIXED_1; // add y^13 / 13 - y^14 / 14 + res += z * (0x088888888888888888888888888888888 - y) / 0x800000000000000000000000000000000; + // add y^15 / 15 - y^16 / 16 + + return res; + } + + /** + Return e ^ (x / FIXED_1) * FIXED_1 + Input range: 0 <= x <= OPT_EXP_MAX_VAL - 1 + Auto-generated via 'PrintFunctionOptimalExp.py' + Detailed description: + - Rewrite the input as a sum of binary exponents and a single residual r, as small as possible + - The exponentiation of each binary exponent is given (pre-calculated) + - The exponentiation of r is calculated via Taylor series for e^x, where x = r + - The exponentiation of the input is calculated by multiplying the intermediate results above + - For example: e^5.521692859 = e^(4 + 1 + 0.5 + 0.021692859) = e^4 * e^1 * e^0.5 * e^0.021692859 + */ + function optimalExp(uint256 x) internal pure returns (uint256) { + uint256 res = 0; + + uint256 y = 0; + uint256 z; + + z = y = x % 0x10000000000000000000000000000000; // get the input modulo 2^(-3) + z = z * y / FIXED_1; + res += z * 0x10e1b3be415a0000; // add y^02 * (20! / 02!) + z = z * y / FIXED_1; + res += z * 0x05a0913f6b1e0000; // add y^03 * (20! / 03!) + z = z * y / FIXED_1; + res += z * 0x0168244fdac78000; // add y^04 * (20! / 04!) + z = z * y / FIXED_1; + res += z * 0x004807432bc18000; // add y^05 * (20! / 05!) + z = z * y / FIXED_1; + res += z * 0x000c0135dca04000; // add y^06 * (20! / 06!) + z = z * y / FIXED_1; + res += z * 0x0001b707b1cdc000; // add y^07 * (20! / 07!) + z = z * y / FIXED_1; + res += z * 0x000036e0f639b800; // add y^08 * (20! / 08!) + z = z * y / FIXED_1; + res += z * 0x00000618fee9f800; // add y^09 * (20! / 09!) + z = z * y / FIXED_1; + res += z * 0x0000009c197dcc00; // add y^10 * (20! / 10!) + z = z * y / FIXED_1; + res += z * 0x0000000e30dce400; // add y^11 * (20! / 11!) + z = z * y / FIXED_1; + res += z * 0x000000012ebd1300; // add y^12 * (20! / 12!) + z = z * y / FIXED_1; + res += z * 0x0000000017499f00; // add y^13 * (20! / 13!) + z = z * y / FIXED_1; + res += z * 0x0000000001a9d480; // add y^14 * (20! / 14!) + z = z * y / FIXED_1; + res += z * 0x00000000001c6380; // add y^15 * (20! / 15!) + z = z * y / FIXED_1; + res += z * 0x000000000001c638; // add y^16 * (20! / 16!) + z = z * y / FIXED_1; + res += z * 0x0000000000001ab8; // add y^17 * (20! / 17!) + z = z * y / FIXED_1; + res += z * 0x000000000000017c; // add y^18 * (20! / 18!) + z = z * y / FIXED_1; + res += z * 0x0000000000000014; // add y^19 * (20! / 19!) + z = z * y / FIXED_1; + res += z * 0x0000000000000001; // add y^20 * (20! / 20!) + res = res / 0x21c3677c82b40000 + y + FIXED_1; // divide by 20! and then add y^1 / 1! + y^0 / 0! + + if ((x & 0x010000000000000000000000000000000) != 0) + res = res * 0x1c3d6a24ed82218787d624d3e5eba95f9 / 0x18ebef9eac820ae8682b9793ac6d1e776; // multiply by e^2^(-3) + if ((x & 0x020000000000000000000000000000000) != 0) + res = res * 0x18ebef9eac820ae8682b9793ac6d1e778 / 0x1368b2fc6f9609fe7aceb46aa619baed4; // multiply by e^2^(-2) + if ((x & 0x040000000000000000000000000000000) != 0) + res = res * 0x1368b2fc6f9609fe7aceb46aa619baed5 / 0x0bc5ab1b16779be3575bd8f0520a9f21f; // multiply by e^2^(-1) + if ((x & 0x080000000000000000000000000000000) != 0) + res = res * 0x0bc5ab1b16779be3575bd8f0520a9f21e / 0x0454aaa8efe072e7f6ddbab84b40a55c9; // multiply by e^2^(+0) + if ((x & 0x100000000000000000000000000000000) != 0) + res = res * 0x0454aaa8efe072e7f6ddbab84b40a55c5 / 0x00960aadc109e7a3bf4578099615711ea; // multiply by e^2^(+1) + if ((x & 0x200000000000000000000000000000000) != 0) + res = res * 0x00960aadc109e7a3bf4578099615711d7 / 0x0002bf84208204f5977f9a8cf01fdce3d; // multiply by e^2^(+2) + if ((x & 0x400000000000000000000000000000000) != 0) + res = res * 0x0002bf84208204f5977f9a8cf01fdc307 / 0x0000003c6ab775dd0b95b4cbee7e65d11; // multiply by e^2^(+3) + + return res; + } +} \ No newline at end of file diff --git a/contracts/utils/SafeMath.sol b/contracts/utils/SafeMath.sol new file mode 100644 index 0000000..1528830 --- /dev/null +++ b/contracts/utils/SafeMath.sol @@ -0,0 +1,56 @@ +pragma solidity ^0.5.2; + + +library SafeMath { + /** + @dev returns the sum of _x and _y, reverts if the calculation overflows + @param _x value 1 + @param _y value 2 + @return sum + */ + function add(uint256 _x, uint256 _y) internal pure returns (uint256) { + uint256 z = _x + _y; + require(z >= _x, "SafeMath failed"); + return z; + } + + /** + @dev returns the difference of _x minus _y, reverts if the calculation underflows + @param _x minuend + @param _y subtrahend + @return difference + */ + function sub(uint256 _x, uint256 _y) internal pure returns (uint256) { + require(_x >= _y, "SafeMath failed"); + return _x - _y; + } + + /** + @dev returns the product of multiplying _x by _y, reverts if the calculation overflows + @param _x factor 1 + @param _y factor 2 + @return product + */ + function mul(uint256 _x, uint256 _y) internal pure returns (uint256) { + // gas optimization + if (_x == 0) + return 0; + + uint256 z = _x * _y; + require(z / _x == _y, "SafeMath failed"); + return z; + } + + /** + @dev Integer division of two numbers truncating the quotient, reverts on division by zero. + @param _x dividend + @param _y divisor + @return quotient + */ + function div(uint256 _x, uint256 _y) internal pure returns (uint256) { + require(_y > 0, "SafeMath failed"); + uint256 c = _x / _y; + + return c; + } +} \ No newline at end of file diff --git a/embark.json b/embark.json new file mode 100644 index 0000000..870cde6 --- /dev/null +++ b/embark.json @@ -0,0 +1,26 @@ +{ + "contracts": [ + "contracts/**" + ], + "buildDir": "dist/", + "config": "config/", + "versions": { + "web3": "1.0.0-beta", + "solc": "0.5.2", + "ipfs-api": "17.2.4" + }, + "plugins": { + "embark-solium": {}, + "embarkjs-connector-web3": {}, + "@trailofbits/embark-contract-info": { + "flags": "" + } + }, + "options": { + "solc": { + "optimize": true, + "optimize-runs": 200 + } + }, + "generationDir": "embarkArtifacts" +} \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..b30cd20 --- /dev/null +++ b/package.json @@ -0,0 +1,77 @@ +{ + "name": "discover-dapps", + "homepage": "https://dap.ps", + "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", + "embark": "^4.0.2", + "embark-solium": "0.0.1", + "decimal.js": "^10.0.2", + "history": "^4.7.2", + "moment": "^2.24.0", + "node-sass": "^4.11.0", + "prop-types": "^15.7.2", + "rc-slider": "8.6.9", + "rc-tooltip": "3.7.3", + "react": "^16.8.4", + "react-content-loader": "^4.2.1", + "react-dom": "^16.8.4", + "react-image-fallback": "^8.0.0", + "react-redux": "^6.0.1", + "react-router": "^4.3.1", + "react-router-dom": "^4.3.1", + "react-scripts": "2.1.8", + "redux": "^4.0.1", + "redux-thunk": "^2.3.0", + "reselect": "^4.0.0", + "web3-utils": "^1.0.0-beta.35", + "webpack": "4.28.3" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject", + "predeploy": "npm run build", + "deploy": "gh-pages -d build", + "slither": "slither . --exclude naming-convention --filter-paths token" + }, + "husky": { + "hooks": { + "pre-commit": "lint-staged" + } + }, + "lint-staged": { + "src/**/*.{js,jsx,ts,tsx,json,css,scss,md}": [ + "prettier --single-quote --write", + "git add" + ] + }, + "eslintConfig": { + "extends": "react-app" + }, + "browserslist": [ + ">0.2%", + "not dead", + "not ie <= 11", + "not op_mini all" + ], + "devDependencies": { + "bignumber.js": "^8.1.1", + "embarkjs-connector-web3": "^4.0.0", + "eslint-config-airbnb": "^17.1.0", + "eslint-config-prettier": "^4.1.0", + "eslint-plugin-prettier": "^3.0.1", + "gh-pages": "^2.0.1", + "husky": "^1.3.1", + "lint-staged": "^8.1.5", + "prettier": "^1.16.4", + "webpack": "4.28.3" + } +} diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..c371f52 Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/fonts/Inter-Black.woff b/public/fonts/Inter-Black.woff new file mode 100644 index 0000000..b2c4e27 Binary files /dev/null and b/public/fonts/Inter-Black.woff differ diff --git a/public/fonts/Inter-Black.woff2 b/public/fonts/Inter-Black.woff2 new file mode 100644 index 0000000..5e7bd9f Binary files /dev/null and b/public/fonts/Inter-Black.woff2 differ diff --git a/public/fonts/Inter-BlackItalic.woff b/public/fonts/Inter-BlackItalic.woff new file mode 100644 index 0000000..ff10955 Binary files /dev/null and b/public/fonts/Inter-BlackItalic.woff differ diff --git a/public/fonts/Inter-BlackItalic.woff2 b/public/fonts/Inter-BlackItalic.woff2 new file mode 100644 index 0000000..97f3c0b Binary files /dev/null and b/public/fonts/Inter-BlackItalic.woff2 differ diff --git a/public/fonts/Inter-Bold.woff b/public/fonts/Inter-Bold.woff new file mode 100644 index 0000000..43dfb67 Binary files /dev/null and b/public/fonts/Inter-Bold.woff differ diff --git a/public/fonts/Inter-Bold.woff2 b/public/fonts/Inter-Bold.woff2 new file mode 100644 index 0000000..b26180b Binary files /dev/null and b/public/fonts/Inter-Bold.woff2 differ diff --git a/public/fonts/Inter-BoldItalic.woff b/public/fonts/Inter-BoldItalic.woff new file mode 100644 index 0000000..0aa33d0 Binary files /dev/null and b/public/fonts/Inter-BoldItalic.woff differ diff --git a/public/fonts/Inter-BoldItalic.woff2 b/public/fonts/Inter-BoldItalic.woff2 new file mode 100644 index 0000000..07ad99d Binary files /dev/null and b/public/fonts/Inter-BoldItalic.woff2 differ diff --git a/public/fonts/Inter-ExtraBold.woff b/public/fonts/Inter-ExtraBold.woff new file mode 100644 index 0000000..a814de5 Binary files /dev/null and b/public/fonts/Inter-ExtraBold.woff differ diff --git a/public/fonts/Inter-ExtraBold.woff2 b/public/fonts/Inter-ExtraBold.woff2 new file mode 100644 index 0000000..fc1e3e2 Binary files /dev/null and b/public/fonts/Inter-ExtraBold.woff2 differ diff --git a/public/fonts/Inter-ExtraBoldItalic.woff b/public/fonts/Inter-ExtraBoldItalic.woff new file mode 100644 index 0000000..6eaf0b2 Binary files /dev/null and b/public/fonts/Inter-ExtraBoldItalic.woff differ diff --git a/public/fonts/Inter-ExtraBoldItalic.woff2 b/public/fonts/Inter-ExtraBoldItalic.woff2 new file mode 100644 index 0000000..79a2452 Binary files /dev/null and b/public/fonts/Inter-ExtraBoldItalic.woff2 differ diff --git a/public/fonts/Inter-ExtraLight-BETA.woff b/public/fonts/Inter-ExtraLight-BETA.woff new file mode 100644 index 0000000..131a66f Binary files /dev/null and b/public/fonts/Inter-ExtraLight-BETA.woff differ diff --git a/public/fonts/Inter-ExtraLight-BETA.woff2 b/public/fonts/Inter-ExtraLight-BETA.woff2 new file mode 100644 index 0000000..e080a70 Binary files /dev/null and b/public/fonts/Inter-ExtraLight-BETA.woff2 differ diff --git a/public/fonts/Inter-ExtraLightItalic-BETA.woff b/public/fonts/Inter-ExtraLightItalic-BETA.woff new file mode 100644 index 0000000..257b53a Binary files /dev/null and b/public/fonts/Inter-ExtraLightItalic-BETA.woff differ diff --git a/public/fonts/Inter-ExtraLightItalic-BETA.woff2 b/public/fonts/Inter-ExtraLightItalic-BETA.woff2 new file mode 100644 index 0000000..0ccb9b6 Binary files /dev/null and b/public/fonts/Inter-ExtraLightItalic-BETA.woff2 differ diff --git a/public/fonts/Inter-Italic.woff b/public/fonts/Inter-Italic.woff new file mode 100644 index 0000000..7e07d71 Binary files /dev/null and b/public/fonts/Inter-Italic.woff differ diff --git a/public/fonts/Inter-Italic.woff2 b/public/fonts/Inter-Italic.woff2 new file mode 100644 index 0000000..435fe82 Binary files /dev/null and b/public/fonts/Inter-Italic.woff2 differ diff --git a/public/fonts/Inter-Light-BETA.woff b/public/fonts/Inter-Light-BETA.woff new file mode 100644 index 0000000..0b46abc Binary files /dev/null and b/public/fonts/Inter-Light-BETA.woff differ diff --git a/public/fonts/Inter-Light-BETA.woff2 b/public/fonts/Inter-Light-BETA.woff2 new file mode 100644 index 0000000..c5fcddd Binary files /dev/null and b/public/fonts/Inter-Light-BETA.woff2 differ diff --git a/public/fonts/Inter-LightItalic-BETA.woff b/public/fonts/Inter-LightItalic-BETA.woff new file mode 100644 index 0000000..d1101cf Binary files /dev/null and b/public/fonts/Inter-LightItalic-BETA.woff differ diff --git a/public/fonts/Inter-LightItalic-BETA.woff2 b/public/fonts/Inter-LightItalic-BETA.woff2 new file mode 100644 index 0000000..fafad9a Binary files /dev/null and b/public/fonts/Inter-LightItalic-BETA.woff2 differ diff --git a/public/fonts/Inter-Medium.woff b/public/fonts/Inter-Medium.woff new file mode 100644 index 0000000..15079dc Binary files /dev/null and b/public/fonts/Inter-Medium.woff differ diff --git a/public/fonts/Inter-Medium.woff2 b/public/fonts/Inter-Medium.woff2 new file mode 100644 index 0000000..7d0fbe9 Binary files /dev/null and b/public/fonts/Inter-Medium.woff2 differ diff --git a/public/fonts/Inter-MediumItalic.woff b/public/fonts/Inter-MediumItalic.woff new file mode 100644 index 0000000..7d8c122 Binary files /dev/null and b/public/fonts/Inter-MediumItalic.woff differ diff --git a/public/fonts/Inter-MediumItalic.woff2 b/public/fonts/Inter-MediumItalic.woff2 new file mode 100644 index 0000000..fa86742 Binary files /dev/null and b/public/fonts/Inter-MediumItalic.woff2 differ diff --git a/public/fonts/Inter-Regular.woff b/public/fonts/Inter-Regular.woff new file mode 100644 index 0000000..e8587fd Binary files /dev/null and b/public/fonts/Inter-Regular.woff differ diff --git a/public/fonts/Inter-Regular.woff2 b/public/fonts/Inter-Regular.woff2 new file mode 100644 index 0000000..46568fd Binary files /dev/null and b/public/fonts/Inter-Regular.woff2 differ diff --git a/public/fonts/Inter-SemiBold.woff b/public/fonts/Inter-SemiBold.woff new file mode 100644 index 0000000..de40b73 Binary files /dev/null and b/public/fonts/Inter-SemiBold.woff differ diff --git a/public/fonts/Inter-SemiBold.woff2 b/public/fonts/Inter-SemiBold.woff2 new file mode 100644 index 0000000..6bb8bee Binary files /dev/null and b/public/fonts/Inter-SemiBold.woff2 differ diff --git a/public/fonts/Inter-SemiBoldItalic.woff b/public/fonts/Inter-SemiBoldItalic.woff new file mode 100644 index 0000000..47b0df3 Binary files /dev/null and b/public/fonts/Inter-SemiBoldItalic.woff differ diff --git a/public/fonts/Inter-SemiBoldItalic.woff2 b/public/fonts/Inter-SemiBoldItalic.woff2 new file mode 100644 index 0000000..9bfbdb8 Binary files /dev/null and b/public/fonts/Inter-SemiBoldItalic.woff2 differ diff --git a/public/fonts/Inter-Thin-BETA.woff b/public/fonts/Inter-Thin-BETA.woff new file mode 100644 index 0000000..46d1472 Binary files /dev/null and b/public/fonts/Inter-Thin-BETA.woff differ diff --git a/public/fonts/Inter-Thin-BETA.woff2 b/public/fonts/Inter-Thin-BETA.woff2 new file mode 100644 index 0000000..382a464 Binary files /dev/null and b/public/fonts/Inter-Thin-BETA.woff2 differ diff --git a/public/fonts/Inter-ThinItalic-BETA.woff b/public/fonts/Inter-ThinItalic-BETA.woff new file mode 100644 index 0000000..6c284f3 Binary files /dev/null and b/public/fonts/Inter-ThinItalic-BETA.woff differ diff --git a/public/fonts/Inter-ThinItalic-BETA.woff2 b/public/fonts/Inter-ThinItalic-BETA.woff2 new file mode 100644 index 0000000..f9471a0 Binary files /dev/null and b/public/fonts/Inter-ThinItalic-BETA.woff2 differ diff --git a/public/fonts/Inter-italic.var.woff2 b/public/fonts/Inter-italic.var.woff2 new file mode 100644 index 0000000..83fb631 Binary files /dev/null and b/public/fonts/Inter-italic.var.woff2 differ diff --git a/public/fonts/Inter-upright.var.woff2 b/public/fonts/Inter-upright.var.woff2 new file mode 100644 index 0000000..db2b70c Binary files /dev/null and b/public/fonts/Inter-upright.var.woff2 differ diff --git a/public/fonts/Inter.var.woff2 b/public/fonts/Inter.var.woff2 new file mode 100644 index 0000000..e8d4309 Binary files /dev/null and b/public/fonts/Inter.var.woff2 differ diff --git a/public/images/dapps/3Box.png b/public/images/dapps/3Box.png new file mode 100644 index 0000000..649fffc Binary files /dev/null and b/public/images/dapps/3Box.png differ diff --git a/public/images/dapps/Dentacoin.png b/public/images/dapps/Dentacoin.png new file mode 100644 index 0000000..9040a45 Binary files /dev/null and b/public/images/dapps/Dentacoin.png differ diff --git a/public/images/dapps/airswap.png b/public/images/dapps/airswap.png new file mode 100644 index 0000000..c6eb532 Binary files /dev/null and b/public/images/dapps/airswap.png differ diff --git a/public/images/dapps/aragon.png b/public/images/dapps/aragon.png new file mode 100644 index 0000000..27c70c5 Binary files /dev/null and b/public/images/dapps/aragon.png differ diff --git a/public/images/dapps/astroledger.svg b/public/images/dapps/astroledger.svg new file mode 100644 index 0000000..f28583b --- /dev/null +++ b/public/images/dapps/astroledger.svg @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/dapps/augur.png b/public/images/dapps/augur.png new file mode 100644 index 0000000..5228e02 Binary files /dev/null and b/public/images/dapps/augur.png differ diff --git a/public/images/dapps/augur.svg b/public/images/dapps/augur.svg new file mode 100644 index 0000000..a5ad5cb --- /dev/null +++ b/public/images/dapps/augur.svg @@ -0,0 +1 @@ +Logo \ No newline at end of file diff --git a/public/images/dapps/bancor.png b/public/images/dapps/bancor.png new file mode 100644 index 0000000..291377f Binary files /dev/null and b/public/images/dapps/bancor.png differ diff --git a/public/images/dapps/bchat.png b/public/images/dapps/bchat.png new file mode 100644 index 0000000..a31e185 Binary files /dev/null and b/public/images/dapps/bchat.png differ diff --git a/public/images/dapps/bidali.png b/public/images/dapps/bidali.png new file mode 100644 index 0000000..27ddedd Binary files /dev/null and b/public/images/dapps/bidali.png differ diff --git a/public/images/dapps/blockimmo.png b/public/images/dapps/blockimmo.png new file mode 100644 index 0000000..ab82371 Binary files /dev/null and b/public/images/dapps/blockimmo.png differ diff --git a/public/images/dapps/bounties-network.png b/public/images/dapps/bounties-network.png new file mode 100644 index 0000000..c0f27c7 Binary files /dev/null and b/public/images/dapps/bounties-network.png differ diff --git a/public/images/dapps/cent.png b/public/images/dapps/cent.png new file mode 100644 index 0000000..3ea6c9b Binary files /dev/null and b/public/images/dapps/cent.png differ diff --git a/public/images/dapps/civitas.png b/public/images/dapps/civitas.png new file mode 100644 index 0000000..bd53976 Binary files /dev/null and b/public/images/dapps/civitas.png differ diff --git a/public/images/dapps/commiteth.png b/public/images/dapps/commiteth.png new file mode 100644 index 0000000..8a43c9b Binary files /dev/null and b/public/images/dapps/commiteth.png differ diff --git a/public/images/dapps/compoundfinance.png b/public/images/dapps/compoundfinance.png new file mode 100644 index 0000000..eb3637a Binary files /dev/null and b/public/images/dapps/compoundfinance.png differ diff --git a/public/images/dapps/console.png b/public/images/dapps/console.png new file mode 100644 index 0000000..9aa3dea Binary files /dev/null and b/public/images/dapps/console.png differ diff --git a/public/images/dapps/cryptocare.jpg b/public/images/dapps/cryptocare.jpg new file mode 100644 index 0000000..99ea73a Binary files /dev/null and b/public/images/dapps/cryptocare.jpg differ diff --git a/public/images/dapps/cryptocribs.png b/public/images/dapps/cryptocribs.png new file mode 100644 index 0000000..6f82c2b Binary files /dev/null and b/public/images/dapps/cryptocribs.png differ diff --git a/public/images/dapps/cryptofighters.png b/public/images/dapps/cryptofighters.png new file mode 100644 index 0000000..cbf5df6 Binary files /dev/null and b/public/images/dapps/cryptofighters.png differ diff --git a/public/images/dapps/cryptographics.png b/public/images/dapps/cryptographics.png new file mode 100644 index 0000000..6fc9e17 Binary files /dev/null and b/public/images/dapps/cryptographics.png differ diff --git a/public/images/dapps/cryptokitties.png b/public/images/dapps/cryptokitties.png new file mode 100644 index 0000000..4a93ca5 Binary files /dev/null and b/public/images/dapps/cryptokitties.png differ diff --git a/public/images/dapps/cryptopunks.png b/public/images/dapps/cryptopunks.png new file mode 100644 index 0000000..be0acdc Binary files /dev/null and b/public/images/dapps/cryptopunks.png differ diff --git a/public/images/dapps/cryptopurr.png b/public/images/dapps/cryptopurr.png new file mode 100644 index 0000000..1385610 Binary files /dev/null and b/public/images/dapps/cryptopurr.png differ diff --git a/public/images/dapps/cryptostrikers.png b/public/images/dapps/cryptostrikers.png new file mode 100644 index 0000000..b3ad4db Binary files /dev/null and b/public/images/dapps/cryptostrikers.png differ diff --git a/public/images/dapps/cryptotakeovers.png b/public/images/dapps/cryptotakeovers.png new file mode 100644 index 0000000..fe7c988 Binary files /dev/null and b/public/images/dapps/cryptotakeovers.png differ diff --git a/public/images/dapps/dBay.png b/public/images/dapps/dBay.png new file mode 100644 index 0000000..2379ebd Binary files /dev/null and b/public/images/dapps/dBay.png differ diff --git a/public/images/dapps/dai.png b/public/images/dapps/dai.png new file mode 100644 index 0000000..8a3abc2 Binary files /dev/null and b/public/images/dapps/dai.png differ diff --git a/public/images/dapps/ddex.png b/public/images/dapps/ddex.png new file mode 100644 index 0000000..27901ea Binary files /dev/null and b/public/images/dapps/ddex.png differ diff --git a/public/images/dapps/decentraland.png b/public/images/dapps/decentraland.png new file mode 100644 index 0000000..e3bd381 Binary files /dev/null and b/public/images/dapps/decentraland.png differ diff --git a/public/images/dapps/dragonereum.png b/public/images/dapps/dragonereum.png new file mode 100644 index 0000000..a8ddb6e Binary files /dev/null and b/public/images/dapps/dragonereum.png differ diff --git a/public/images/dapps/easytrade.png b/public/images/dapps/easytrade.png new file mode 100644 index 0000000..5ce6275 Binary files /dev/null and b/public/images/dapps/easytrade.png differ diff --git a/public/images/dapps/emoon.png b/public/images/dapps/emoon.png new file mode 100644 index 0000000..d250ff4 Binary files /dev/null and b/public/images/dapps/emoon.png differ diff --git a/public/images/dapps/eth2phone.png b/public/images/dapps/eth2phone.png new file mode 100644 index 0000000..11e7594 Binary files /dev/null and b/public/images/dapps/eth2phone.png differ diff --git a/public/images/dapps/ethcro.png b/public/images/dapps/ethcro.png new file mode 100644 index 0000000..5fb9ff4 Binary files /dev/null and b/public/images/dapps/ethcro.png differ diff --git a/public/images/dapps/etherbots.png b/public/images/dapps/etherbots.png new file mode 100644 index 0000000..e9d265b Binary files /dev/null and b/public/images/dapps/etherbots.png differ diff --git a/public/images/dapps/etheremon.png b/public/images/dapps/etheremon.png new file mode 100644 index 0000000..9f9ff49 Binary files /dev/null and b/public/images/dapps/etheremon.png differ diff --git a/public/images/dapps/etherman.png b/public/images/dapps/etherman.png new file mode 100644 index 0000000..2bebacd Binary files /dev/null and b/public/images/dapps/etherman.png differ diff --git a/public/images/dapps/etherplay.png b/public/images/dapps/etherplay.png new file mode 100644 index 0000000..52992ee Binary files /dev/null and b/public/images/dapps/etherplay.png differ diff --git a/public/images/dapps/ethlance.png b/public/images/dapps/ethlance.png new file mode 100644 index 0000000..80db5d7 Binary files /dev/null and b/public/images/dapps/ethlance.png differ diff --git a/public/images/dapps/ethlend.png b/public/images/dapps/ethlend.png new file mode 100644 index 0000000..c35ec23 Binary files /dev/null and b/public/images/dapps/ethlend.png differ diff --git a/public/images/dapps/expotrading.png b/public/images/dapps/expotrading.png new file mode 100644 index 0000000..2239e01 Binary files /dev/null and b/public/images/dapps/expotrading.png differ diff --git a/public/images/dapps/fairhouse.png b/public/images/dapps/fairhouse.png new file mode 100644 index 0000000..54ed24e Binary files /dev/null and b/public/images/dapps/fairhouse.png differ diff --git a/public/images/dapps/gnosis.png b/public/images/dapps/gnosis.png new file mode 100644 index 0000000..abf790e Binary files /dev/null and b/public/images/dapps/gnosis.png differ diff --git a/public/images/dapps/hexel.png b/public/images/dapps/hexel.png new file mode 100644 index 0000000..a2f7864 Binary files /dev/null and b/public/images/dapps/hexel.png differ diff --git a/public/images/dapps/instadapp.jpg b/public/images/dapps/instadapp.jpg new file mode 100644 index 0000000..5a69bfa Binary files /dev/null and b/public/images/dapps/instadapp.jpg differ diff --git a/public/images/dapps/kickback.png b/public/images/dapps/kickback.png new file mode 100644 index 0000000..b6cd2b1 Binary files /dev/null and b/public/images/dapps/kickback.png differ diff --git a/public/images/dapps/knownorigin.png b/public/images/dapps/knownorigin.png new file mode 100644 index 0000000..b5686f5 Binary files /dev/null and b/public/images/dapps/knownorigin.png differ diff --git a/public/images/dapps/kyber.png b/public/images/dapps/kyber.png new file mode 100644 index 0000000..016d36b Binary files /dev/null and b/public/images/dapps/kyber.png differ diff --git a/public/images/dapps/livepeer.png b/public/images/dapps/livepeer.png new file mode 100644 index 0000000..d1841c5 Binary files /dev/null and b/public/images/dapps/livepeer.png differ diff --git a/public/images/dapps/local-ethereum.png b/public/images/dapps/local-ethereum.png new file mode 100644 index 0000000..0c76b25 Binary files /dev/null and b/public/images/dapps/local-ethereum.png differ diff --git a/public/images/dapps/melonport.png b/public/images/dapps/melonport.png new file mode 100644 index 0000000..b216a92 Binary files /dev/null and b/public/images/dapps/melonport.png differ diff --git a/public/images/dapps/mkr-market.png b/public/images/dapps/mkr-market.png new file mode 100644 index 0000000..d132a6f Binary files /dev/null and b/public/images/dapps/mkr-market.png differ diff --git a/public/images/dapps/name-bazaar.png b/public/images/dapps/name-bazaar.png new file mode 100644 index 0000000..bf99369 Binary files /dev/null and b/public/images/dapps/name-bazaar.png differ diff --git a/public/images/dapps/nuo.png b/public/images/dapps/nuo.png new file mode 100644 index 0000000..11c5ca7 Binary files /dev/null and b/public/images/dapps/nuo.png differ diff --git a/public/images/dapps/oaken-water-meter.png b/public/images/dapps/oaken-water-meter.png new file mode 100644 index 0000000..c83c115 Binary files /dev/null and b/public/images/dapps/oaken-water-meter.png differ diff --git a/public/images/dapps/opensea.png b/public/images/dapps/opensea.png new file mode 100644 index 0000000..a03cdd7 Binary files /dev/null and b/public/images/dapps/opensea.png differ diff --git a/public/images/dapps/peepeth.png b/public/images/dapps/peepeth.png new file mode 100644 index 0000000..052de5e Binary files /dev/null and b/public/images/dapps/peepeth.png differ diff --git a/public/images/dapps/slowtrade.png b/public/images/dapps/slowtrade.png new file mode 100644 index 0000000..16c12f7 Binary files /dev/null and b/public/images/dapps/slowtrade.png differ diff --git a/public/images/dapps/smartz.png b/public/images/dapps/smartz.png new file mode 100644 index 0000000..19f0c2d Binary files /dev/null and b/public/images/dapps/smartz.png differ diff --git a/public/images/dapps/snt-voting.png b/public/images/dapps/snt-voting.png new file mode 100644 index 0000000..2bc0e1e Binary files /dev/null and b/public/images/dapps/snt-voting.png differ diff --git a/public/images/dapps/superrare.png b/public/images/dapps/superrare.png new file mode 100644 index 0000000..7d8faa4 Binary files /dev/null and b/public/images/dapps/superrare.png differ diff --git a/public/images/dapps/uniswap.png b/public/images/dapps/uniswap.png new file mode 100644 index 0000000..f5881e8 Binary files /dev/null and b/public/images/dapps/uniswap.png differ diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..706a385 --- /dev/null +++ b/public/index.html @@ -0,0 +1,40 @@ + + + + + + + + + + Discover Dapps | Status + + + +
+ + + diff --git a/src/common/assets/images/SNT.svg b/src/common/assets/images/SNT.svg new file mode 100644 index 0000000..257e6e7 --- /dev/null +++ b/src/common/assets/images/SNT.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/common/assets/images/add-dapp.svg b/src/common/assets/images/add-dapp.svg new file mode 100644 index 0000000..f854dfb --- /dev/null +++ b/src/common/assets/images/add-dapp.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/common/assets/images/categories/collectibles.svg b/src/common/assets/images/categories/collectibles.svg new file mode 100755 index 0000000..4431b5a --- /dev/null +++ b/src/common/assets/images/categories/collectibles.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/common/assets/images/categories/exchanges.svg b/src/common/assets/images/categories/exchanges.svg new file mode 100755 index 0000000..21b4b43 --- /dev/null +++ b/src/common/assets/images/categories/exchanges.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/common/assets/images/categories/games.svg b/src/common/assets/images/categories/games.svg new file mode 100755 index 0000000..16ca582 --- /dev/null +++ b/src/common/assets/images/categories/games.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/common/assets/images/categories/marketplaces.svg b/src/common/assets/images/categories/marketplaces.svg new file mode 100755 index 0000000..130e888 --- /dev/null +++ b/src/common/assets/images/categories/marketplaces.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/common/assets/images/categories/other.svg b/src/common/assets/images/categories/other.svg new file mode 100755 index 0000000..a778c05 --- /dev/null +++ b/src/common/assets/images/categories/other.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/common/assets/images/categories/social-networks.svg b/src/common/assets/images/categories/social-networks.svg new file mode 100755 index 0000000..03580ca --- /dev/null +++ b/src/common/assets/images/categories/social-networks.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/common/assets/images/categories/utilities.svg b/src/common/assets/images/categories/utilities.svg new file mode 100755 index 0000000..fa8c652 --- /dev/null +++ b/src/common/assets/images/categories/utilities.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/common/assets/images/chat.svg b/src/common/assets/images/chat.svg new file mode 100644 index 0000000..88355a2 --- /dev/null +++ b/src/common/assets/images/chat.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/common/assets/images/community.svg b/src/common/assets/images/community.svg new file mode 100644 index 0000000..6b1147b --- /dev/null +++ b/src/common/assets/images/community.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/common/assets/images/downvote-arrow.svg b/src/common/assets/images/downvote-arrow.svg new file mode 100644 index 0000000..a9601c1 --- /dev/null +++ b/src/common/assets/images/downvote-arrow.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/common/assets/images/dropdown-arrows.svg b/src/common/assets/images/dropdown-arrows.svg new file mode 100644 index 0000000..db16d38 --- /dev/null +++ b/src/common/assets/images/dropdown-arrows.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/common/assets/images/fallback.svg b/src/common/assets/images/fallback.svg new file mode 100644 index 0000000..fe7589f --- /dev/null +++ b/src/common/assets/images/fallback.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/common/assets/images/featured/airswap_banner.png b/src/common/assets/images/featured/airswap_banner.png new file mode 100644 index 0000000..0725d8e Binary files /dev/null and b/src/common/assets/images/featured/airswap_banner.png differ diff --git a/src/common/assets/images/featured/airswap_logo.png b/src/common/assets/images/featured/airswap_logo.png new file mode 100644 index 0000000..2f7bf05 Binary files /dev/null and b/src/common/assets/images/featured/airswap_logo.png differ diff --git a/src/common/assets/images/featured/cryptokitties_logo.png b/src/common/assets/images/featured/cryptokitties_logo.png new file mode 100644 index 0000000..9f4b443 Binary files /dev/null and b/src/common/assets/images/featured/cryptokitties_logo.png differ diff --git a/src/common/assets/images/featured/crytokittes_banner.png b/src/common/assets/images/featured/crytokittes_banner.png new file mode 100644 index 0000000..23c8d8d Binary files /dev/null and b/src/common/assets/images/featured/crytokittes_banner.png differ diff --git a/src/common/assets/images/featured/kyber_banner.png b/src/common/assets/images/featured/kyber_banner.png new file mode 100755 index 0000000..b75864b Binary files /dev/null and b/src/common/assets/images/featured/kyber_banner.png differ diff --git a/src/common/assets/images/featured/kyber_logo.png b/src/common/assets/images/featured/kyber_logo.png new file mode 100644 index 0000000..016d36b Binary files /dev/null and b/src/common/assets/images/featured/kyber_logo.png differ diff --git a/src/common/assets/images/icon.svg b/src/common/assets/images/icon.svg new file mode 100644 index 0000000..704a3ac --- /dev/null +++ b/src/common/assets/images/icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/common/assets/images/loading-spinner.svg b/src/common/assets/images/loading-spinner.svg new file mode 100644 index 0000000..6e168f7 --- /dev/null +++ b/src/common/assets/images/loading-spinner.svg @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/common/assets/images/support.svg b/src/common/assets/images/support.svg new file mode 100644 index 0000000..a6ffbd1 --- /dev/null +++ b/src/common/assets/images/support.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/common/assets/images/upvote-arrow.svg b/src/common/assets/images/upvote-arrow.svg new file mode 100644 index 0000000..74547fd --- /dev/null +++ b/src/common/assets/images/upvote-arrow.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/common/blockchain/index.js b/src/common/blockchain/index.js new file mode 100644 index 0000000..f14f274 --- /dev/null +++ b/src/common/blockchain/index.js @@ -0,0 +1,48 @@ +/* global web3 */ +import utils from './utils' +import EmbarkJS from '../../embarkArtifacts/embarkjs' + +import * as IPFSService from './ipfs' +import SNTService from './services/contracts-services/snt-service/snt-service' +import DiscoverService from './services/contracts-services/discover-service/discover-service' + +const initServices = function() { + const sharedContext = { + account: '0x0000000000000000000000000000000000000000', + } + + sharedContext.SNTService = new SNTService(sharedContext) + sharedContext.DiscoverService = new DiscoverService(sharedContext) + + return { + IPFSService, + SNTService: sharedContext.SNTService, + DiscoverService: sharedContext.DiscoverService, + utils, + } +} + +const getInstance = async () => { + return new Promise((resolve, reject) => { + const returnInstance = () => { + try { + const services = initServices() + resolve(services) + } catch (error) { + reject(error.message) + } + } + + if (web3.currentProvider) { + returnInstance() + } else { + EmbarkJS.onReady(err => { + if (err) reject(err) + + returnInstance() + }) + } + }) +} + +export default { getInstance, utils } diff --git a/src/common/blockchain/ipfs/helpers.js b/src/common/blockchain/ipfs/helpers.js new file mode 100644 index 0000000..70bc641 --- /dev/null +++ b/src/common/blockchain/ipfs/helpers.js @@ -0,0 +1,28 @@ +import bs58 from 'bs58' + +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]) +} + +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 new file mode 100644 index 0000000..d10d025 --- /dev/null +++ b/src/common/blockchain/ipfs/index.js @@ -0,0 +1,63 @@ +import * as helpers from './helpers' +import EmbarkJS from '../../../embarkArtifacts/embarkjs' + +const checkIPFSAvailability = async () => { + const isAvailable = await EmbarkJS.Storage.isAvailable() + if (!isAvailable) { + throw new Error('IPFS Storage is unavailable') + } +} + +const uploadImage = async base64Image => { + const imageFile = [ + { + files: [helpers.base64ToBlob(base64Image)], + }, + ] + + return EmbarkJS.Storage.uploadFile(imageFile) +} + +const uploadMetadata = async metadata => { + const hash = await EmbarkJS.Storage.saveText(metadata) + return helpers.getBytes32FromIpfsHash(hash) +} + +export const uploadDAppMetadata = async metadata => { + try { + await checkIPFSAvailability() + + metadata.image = await uploadImage(metadata.image) + const uploadedMetadataHash = await uploadMetadata(JSON.stringify(metadata)) + + return uploadedMetadataHash + } catch (error) { + throw new Error( + `Uploading DApp metadata to IPFS failed. Details: ${error.message}`, + ) + } +} + +const retrieveMetadata = async metadataBytes32 => { + const metadataHash = helpers.getIpfsHashFromBytes32(metadataBytes32) + return EmbarkJS.Storage.get(metadataHash) +} + +const retrieveImageUrl = async imageHash => { + return EmbarkJS.Storage.getUrl(imageHash) +} + +export const retrieveDAppMetadataByHash = async metadataBytes32 => { + try { + await checkIPFSAvailability() + + const metadata = JSON.parse(await retrieveMetadata(metadataBytes32)) + metadata.image = await retrieveImageUrl(metadata.image) + + return metadata + } catch (error) { + throw new Error( + `Fetching metadata from IPFS failed. Details: ${error.message}`, + ) + } +} diff --git a/src/common/blockchain/services/contracts-services/blockchain-service.js b/src/common/blockchain/services/contracts-services/blockchain-service.js new file mode 100644 index 0000000..602b21b --- /dev/null +++ b/src/common/blockchain/services/contracts-services/blockchain-service.js @@ -0,0 +1,35 @@ +/* global web3 */ + +import EmbarkJS from '../../../../embarkArtifacts/embarkjs' + +const getAccount = async () => { + try { + const account = (await EmbarkJS.enableEthereum())[0] + return ( + account || (await EmbarkJS.Blockchain.Providers.web3.getAccounts())[0] + ) + } catch (error) { + throw new Error( + 'Could not unlock an account. Consider installing Status on your mobile or Metamask extension', + ) + } +} + +class BlockchainService { + constructor(sharedContext, contract, Validator) { + this.contract = contract.address + + this.sharedContext = sharedContext + this.validator = new Validator(this) + } + + async __unlockServiceAccount() { + if (web3 && EmbarkJS.Blockchain.Providers.web3) { + this.sharedContext.account = await getAccount() + } else { + throw new Error('web3 is missing') + } + } +} + +export default BlockchainService 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 new file mode 100644 index 0000000..e2f1d99 --- /dev/null +++ b/src/common/blockchain/services/contracts-services/discover-service/discover-service.js @@ -0,0 +1,178 @@ +/* global web3 */ +import { broadcastContractFn } from '../helpers' + +import * as ipfsSDK from '../../../ipfs' +import BlockchainService from '../blockchain-service' + +import DiscoverValidator from './discover-validator' +import DiscoverContract from '../../../../../embarkArtifacts/contracts/Discover' + +class DiscoverService extends BlockchainService { + constructor(sharedContext) { + super(sharedContext, DiscoverContract, DiscoverValidator) + } + + // View methods + async upVoteEffect(id, amount) { + await this.validator.validateUpVoteEffect(id, amount) + + return DiscoverContract.methods + .upvoteEffect(id, amount) + .call({ from: this.sharedContext.account }) + } + + async downVoteCost(id) { + const dapp = await this.getDAppById(id) + return DiscoverContract.methods + .downvoteCost(dapp.id) + .call({ from: this.sharedContext.account }) + } + + async getDAppsCount() { + return DiscoverContract.methods + .getDAppsCount() + .call({ from: this.sharedContext.account }) + } + + async getDApps() { + const dapps = [] + const dappsCount = await this.getDAppsCount() + + try { + for (let i = 0; i < dappsCount; i++) { + const dapp = await DiscoverContract.methods + .dapps(i) + .call({ from: this.sharedContext.account }) + + dapp.metadata = await ipfsSDK.retrieveDAppMetadataByHash(dapp.metadata) + dapps.push(dapp) + } + return dapps + } catch (error) { + throw new Error(`Error fetching dapps. Details: ${error.message}`) + } + } + + async getDAppById(id) { + let dapp + try { + const dappId = await DiscoverContract.methods + .id2index(id) + .call({ from: this.sharedContext.account }) + dapp = await DiscoverContract.methods + .dapps(dappId) + .call({ from: this.sharedContext.account }) + } catch (error) { + throw new Error('Searching DApp does not exists') + } + + if (dapp.id != id) { + throw new Error('Error fetching correct data from contract') + } + + return dapp + } + + async getDAppDataById(id) { + const dapp = await this.getDAppById(id) + + try { + dapp.metadata = await ipfsSDK.retrieveDAppMetadataByHash(dapp.metadata) + return dapp + } catch (error) { + throw new Error('Error fetching correct data from IPFS') + } + } + + async safeMax() { + return DiscoverContract.methods + .safeMax() + .call({ from: this.sharedContext.account }) + } + + async isDAppExists(id) { + return DiscoverContract.methods + .existingIDs(id) + .call({ from: this.sharedContext.account }) + } + + // Transaction methods + async createDApp(amount, metadata) { + const dappMetadata = JSON.parse(JSON.stringify(metadata)) + const dappId = web3.utils.keccak256(JSON.stringify(dappMetadata)) + + await this.validator.validateDAppCreation(dappId, amount) + + const uploadedMetadata = await ipfsSDK.uploadDAppMetadata(dappMetadata) + + const callData = DiscoverContract.methods + .createDApp(dappId, amount, uploadedMetadata) + .encodeABI() + + const createdTx = await this.sharedContext.SNTService.approveAndCall( + this.contract, + amount, + callData, + ) + + return { tx: createdTx, id: dappId } + } + + async upVote(id, amount) { + await this.validator.validateUpVoting(id, amount) + + const callData = DiscoverContract.methods.upvote(id, amount).encodeABI() + return this.sharedContext.SNTService.approveAndCall( + this.contract, + amount, + callData, + ) + } + + async downVote(id) { + const dapp = await this.getDAppById(id) + const amount = (await this.downVoteCost(dapp.id)).c + + const callData = DiscoverContract.methods + .downvote(dapp.id, amount) + .encodeABI() + return this.sharedContext.SNTService.approveAndCall( + this.contract, + amount, + callData, + ) + } + + async withdraw(id, amount) { + await super.__unlockServiceAccount() + await this.validator.validateWithdrawing(id, amount) + + try { + return broadcastContractFn( + DiscoverContract.methods.withdraw(id, amount).send, + this.sharedContext.account, + ) + } catch (error) { + throw new Error(`Transfer on withdraw failed. Details: ${error.message}`) + } + } + + async setMetadata(id, metadata) { + await super.__unlockServiceAccount() + await this.validator.validateMetadataSet(id) + + const dappMetadata = JSON.parse(JSON.stringify(metadata)) + const uploadedMetadata = await ipfsSDK.uploadDAppMetadata(dappMetadata) + + try { + return broadcastContractFn( + DiscoverContract.methods.setMetadata(id, uploadedMetadata).send, + this.sharedContext.account, + ) + } catch (error) { + throw new Error(`Uploading metadata failed. Details: ${error.message}`) + } + } +} + +export default DiscoverService 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 new file mode 100644 index 0000000..0b21d03 --- /dev/null +++ b/src/common/blockchain/services/contracts-services/discover-service/discover-validator.js @@ -0,0 +1,67 @@ +class DiscoverValidator { + constructor(service) { + this.service = service + } + + async validateUpVoteEffect(id, amount) { + const dapp = await this.service.getDAppById(id) + + const safeMax = await this.service.safeMax() + if (Number(dapp.balance) + amount > safeMax) { + throw new Error( + `You cannot upvote by this much, try with a lower amount. Maximum upvote amount: + ${Number(safeMax) - Number(dapp.balance)}`, + ) + } + } + + async validateDAppCreation(id, amount) { + const dappExists = await this.service.isDAppExists(id) + if (dappExists) { + throw new Error('You must submit a unique ID') + } + + if (amount <= 0) { + throw new Error( + 'You must spend some SNT to submit a ranking in order to avoid spam', + ) + } + + const safeMax = await this.service.safeMax() + if (amount > safeMax) { + throw new Error('You cannot stake more SNT than the ceiling dictates') + } + } + + async validateUpVoting(id, amount) { + await this.validateUpVoteEffect(id, amount) + + if (amount <= 0) { + throw new Error('You must send some SNT in order to upvote') + } + } + + async validateWithdrawing(id, amount) { + const dapp = await this.service.getDAppById(id) + + if (dapp.developer.toLowerCase() != this.service.sharedContext.account) { + throw new Error('Only the developer can withdraw SNT staked on this data') + } + + if (amount > dapp.available) { + throw new Error( + 'You can only withdraw a percentage of the SNT staked, less what you have already received', + ) + } + } + + async validateMetadataSet(id) { + const dapp = await this.service.getDAppById(id) + + if (dapp.developer.toLowerCase() != this.service.sharedContext.account) { + throw new Error('Only the developer can update the metadata') + } + } +} + +export default DiscoverValidator 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..e399061 --- /dev/null +++ b/src/common/blockchain/services/contracts-services/helpers.js @@ -0,0 +1,11 @@ +export const broadcastContractFn = (contractMethod, account) => { + return new Promise((resolve, reject) => { + contractMethod({ from: account }) + .on('transactionHash', hash => { + resolve(hash) + }) + .on('error', error => { + reject(error.message) + }) + }) +} diff --git a/src/common/blockchain/services/contracts-services/snt-service/snt-service.js b/src/common/blockchain/services/contracts-services/snt-service/snt-service.js new file mode 100644 index 0000000..0e4f875 --- /dev/null +++ b/src/common/blockchain/services/contracts-services/snt-service/snt-service.js @@ -0,0 +1,57 @@ +import { broadcastContractFn } from '../helpers' + +import BlockchainService from '../blockchain-service' + +import SNTValidator from './snt-validator' +import SNTToken from '../../../../../embarkArtifacts/contracts/SNT' + +class SNTService extends BlockchainService { + constructor(sharedContext) { + super(sharedContext, SNTToken, SNTValidator) + } + + async allowance(from, to) { + return SNTToken.methods + .allowance(from, to) + .call({ from: this.sharedContext.account }) + } + + async balanceOf(account) { + return SNTToken.methods + .balanceOf(account) + .call({ from: this.sharedContext.account }) + } + + async controller() { + return SNTToken.methods + .controller() + .call({ from: this.sharedContext.account }) + } + + async transferable() { + return SNTToken.methods + .transfersEnabled() + .call({ from: this.sharedContext.account }) + } + + async approveAndCall(spender, amount, callData) { + await super.__unlockServiceAccount() + await this.validator.validateApproveAndCall(spender, amount) + + return broadcastContractFn( + SNTToken.methods.approveAndCall(spender, amount, callData).send, + this.sharedContext.account, + ) + } + + // This is for testing purpose only + async generateTokens() { + await super.__unlockServiceAccount() + + await SNTToken.methods + .generateTokens(this.sharedContext.account, 10000) + .send({ from: this.sharedContext.account }) + } +} + +export default SNTService diff --git a/src/common/blockchain/services/contracts-services/snt-service/snt-validator.js b/src/common/blockchain/services/contracts-services/snt-service/snt-validator.js new file mode 100644 index 0000000..6ba8d1b --- /dev/null +++ b/src/common/blockchain/services/contracts-services/snt-service/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/common/blockchain/utils.js b/src/common/blockchain/utils.js new file mode 100644 index 0000000..78f4139 --- /dev/null +++ b/src/common/blockchain/utils.js @@ -0,0 +1,34 @@ +/* global web3 */ + +const TRANSACTION_STATUSES = { + Failed: 0, + Successful: 1, + Pending: 2, +} + +const waitOneMoreBlock = async function(prevBlockNumber) { + return new Promise(resolve => { + setTimeout(async () => { + const blockNumber = await web3.eth.getBlockNumber() + if (prevBlockNumber == blockNumber) { + return waitOneMoreBlock(prevBlockNumber) + } + resolve() + }, 30000) + }) +} + +export default { + getTxStatus: async txHash => { + const txReceipt = await web3.eth.getTransactionReceipt(txHash) + if (txReceipt) { + await waitOneMoreBlock(txReceipt.blockNumber) + + return txReceipt.status + ? TRANSACTION_STATUSES.Successful + : TRANSACTION_STATUSES.Failed + } + + return TRANSACTION_STATUSES.Pending + }, +} diff --git a/src/common/components/CategoryIcon/CategoryIcon.jsx b/src/common/components/CategoryIcon/CategoryIcon.jsx new file mode 100644 index 0000000..0dc6338 --- /dev/null +++ b/src/common/components/CategoryIcon/CategoryIcon.jsx @@ -0,0 +1,32 @@ +import React from 'react' +import PropTypes from 'prop-types' +import ExchangesIcon from './ExhangesIcon' +import MarketplacesIcon from './MarketplacesIcon' +import GamesIcon from './GamesIcon' +import UtilitiesIcon from './UtilitiesIcon' +import OtherIcon from './OtherIcon' +import CollectiblesIcon from './CollectiblesIcon' +import SocialNetworksIcon from './SocialNetworksIcon' + +const icons = { + EXCHANGES: ExchangesIcon, + MARKETPLACES: MarketplacesIcon, + GAMES: GamesIcon, + UTILITIES: UtilitiesIcon, + OTHER: OtherIcon, + COLLECTIBLES: CollectiblesIcon, + SOCIAL_NETWORKS: SocialNetworksIcon, + MEDIA: GamesIcon, // TODO: Need to get this asset from design +} + +const CategoryIcon = props => { + const { category } = props + const Icon = icons[category] + return +} + +CategoryIcon.propTypes = { + category: PropTypes.string.isRequired, +} + +export default CategoryIcon diff --git a/src/common/components/CategoryIcon/CollectiblesIcon/CollectiblesIcon.jsx b/src/common/components/CategoryIcon/CollectiblesIcon/CollectiblesIcon.jsx new file mode 100644 index 0000000..5f85a95 --- /dev/null +++ b/src/common/components/CategoryIcon/CollectiblesIcon/CollectiblesIcon.jsx @@ -0,0 +1,26 @@ +import React from 'react' + +const icon = () => ( + + + + + + + +) + +export default icon diff --git a/src/common/components/CategoryIcon/CollectiblesIcon/index.js b/src/common/components/CategoryIcon/CollectiblesIcon/index.js new file mode 100644 index 0000000..1d611e2 --- /dev/null +++ b/src/common/components/CategoryIcon/CollectiblesIcon/index.js @@ -0,0 +1,3 @@ +import CollectiblesIcon from './CollectiblesIcon' + +export default CollectiblesIcon diff --git a/src/common/components/CategoryIcon/ExhangesIcon/ExchangesIcon.jsx b/src/common/components/CategoryIcon/ExhangesIcon/ExchangesIcon.jsx new file mode 100644 index 0000000..03dc974 --- /dev/null +++ b/src/common/components/CategoryIcon/ExhangesIcon/ExchangesIcon.jsx @@ -0,0 +1,17 @@ +import React from 'react' + +const icon = () => ( + + + + + + +) + +export default icon diff --git a/src/common/components/CategoryIcon/ExhangesIcon/index.jsx b/src/common/components/CategoryIcon/ExhangesIcon/index.jsx new file mode 100644 index 0000000..b2e15bf --- /dev/null +++ b/src/common/components/CategoryIcon/ExhangesIcon/index.jsx @@ -0,0 +1,3 @@ +import ExchangesIcon from './ExchangesIcon' + +export default ExchangesIcon diff --git a/src/common/components/CategoryIcon/GamesIcon/GamesIcon.jsx b/src/common/components/CategoryIcon/GamesIcon/GamesIcon.jsx new file mode 100644 index 0000000..c90c802 --- /dev/null +++ b/src/common/components/CategoryIcon/GamesIcon/GamesIcon.jsx @@ -0,0 +1,18 @@ +import React from 'react' + +const icon = () => ( + + + +) + +export default icon diff --git a/src/common/components/CategoryIcon/GamesIcon/index.js b/src/common/components/CategoryIcon/GamesIcon/index.js new file mode 100644 index 0000000..c914467 --- /dev/null +++ b/src/common/components/CategoryIcon/GamesIcon/index.js @@ -0,0 +1,3 @@ +import GamesIcon from './GamesIcon' + +export default GamesIcon diff --git a/src/common/components/CategoryIcon/MarketplacesIcon/MarketplacesIcon.jsx b/src/common/components/CategoryIcon/MarketplacesIcon/MarketplacesIcon.jsx new file mode 100644 index 0000000..151e786 --- /dev/null +++ b/src/common/components/CategoryIcon/MarketplacesIcon/MarketplacesIcon.jsx @@ -0,0 +1,16 @@ +import React from 'react' + +const icon = () => ( + + + + + +) + +export default icon diff --git a/src/common/components/CategoryIcon/MarketplacesIcon/index.js b/src/common/components/CategoryIcon/MarketplacesIcon/index.js new file mode 100644 index 0000000..dd9bd25 --- /dev/null +++ b/src/common/components/CategoryIcon/MarketplacesIcon/index.js @@ -0,0 +1,3 @@ +import MarketplacesIcon from './MarketplacesIcon' + +export default MarketplacesIcon diff --git a/src/common/components/CategoryIcon/OtherIcon/OtherIcon.jsx b/src/common/components/CategoryIcon/OtherIcon/OtherIcon.jsx new file mode 100644 index 0000000..08af17c --- /dev/null +++ b/src/common/components/CategoryIcon/OtherIcon/OtherIcon.jsx @@ -0,0 +1,18 @@ +import React from 'react' + +const icon = () => ( + + + +) + +export default icon diff --git a/src/common/components/CategoryIcon/OtherIcon/index.js b/src/common/components/CategoryIcon/OtherIcon/index.js new file mode 100644 index 0000000..ba95414 --- /dev/null +++ b/src/common/components/CategoryIcon/OtherIcon/index.js @@ -0,0 +1,3 @@ +import OtherIcon from './OtherIcon' + +export default OtherIcon diff --git a/src/common/components/CategoryIcon/SocialNetworksIcon/SocialNetworksIcon.jsx b/src/common/components/CategoryIcon/SocialNetworksIcon/SocialNetworksIcon.jsx new file mode 100644 index 0000000..1cc6efd --- /dev/null +++ b/src/common/components/CategoryIcon/SocialNetworksIcon/SocialNetworksIcon.jsx @@ -0,0 +1,18 @@ +import React from 'react' + +const SocialNetworksIcon = () => ( + + + +) + +export default SocialNetworksIcon diff --git a/src/common/components/CategoryIcon/SocialNetworksIcon/index.js b/src/common/components/CategoryIcon/SocialNetworksIcon/index.js new file mode 100644 index 0000000..98856d9 --- /dev/null +++ b/src/common/components/CategoryIcon/SocialNetworksIcon/index.js @@ -0,0 +1,3 @@ +import SocialNetworksIcon from './SocialNetworksIcon' + +export default SocialNetworksIcon diff --git a/src/common/components/CategoryIcon/UtilitiesIcon/UtilitiesIcon.jsx b/src/common/components/CategoryIcon/UtilitiesIcon/UtilitiesIcon.jsx new file mode 100644 index 0000000..ec4257d --- /dev/null +++ b/src/common/components/CategoryIcon/UtilitiesIcon/UtilitiesIcon.jsx @@ -0,0 +1,33 @@ +import React from 'react' + +const icon = () => ( + + + + + + + + + + + + + +) + +export default icon diff --git a/src/common/components/CategoryIcon/UtilitiesIcon/index.js b/src/common/components/CategoryIcon/UtilitiesIcon/index.js new file mode 100644 index 0000000..08b8847 --- /dev/null +++ b/src/common/components/CategoryIcon/UtilitiesIcon/index.js @@ -0,0 +1,3 @@ +import UtilitiesIcon from './UtilitiesIcon' + +export default UtilitiesIcon diff --git a/src/common/components/CategoryIcon/index.js b/src/common/components/CategoryIcon/index.js new file mode 100644 index 0000000..fe7da21 --- /dev/null +++ b/src/common/components/CategoryIcon/index.js @@ -0,0 +1,3 @@ +import CategoryIcon from './CategoryIcon' + +export default CategoryIcon diff --git a/src/common/components/DappList/DappList.jsx b/src/common/components/DappList/DappList.jsx new file mode 100644 index 0000000..9d0f778 --- /dev/null +++ b/src/common/components/DappList/DappList.jsx @@ -0,0 +1,32 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { DappListModel } from '../../utils/models' +import DappListItem from '../DappListItem' + +const DappList = props => { + const { dapps, isRanked, showActionButtons } = props + return ( + dapps && + dapps.map((dapp, i) => ( + + )) + ) +} + +DappList.defaultProps = { + showActionButtons: true, +} + +DappList.propTypes = { + dapps: DappListModel.isRequired, + isRanked: PropTypes.bool, + showActionButtons: PropTypes.bool, +} + +export default DappList diff --git a/src/common/components/DappList/index.js b/src/common/components/DappList/index.js new file mode 100644 index 0000000..c43cdb9 --- /dev/null +++ b/src/common/components/DappList/index.js @@ -0,0 +1,3 @@ +import DappList from './DappList' + +export default DappList diff --git a/src/common/components/DappListItem/DappListItem.container.js b/src/common/components/DappListItem/DappListItem.container.js new file mode 100644 index 0000000..2999999 --- /dev/null +++ b/src/common/components/DappListItem/DappListItem.container.js @@ -0,0 +1,27 @@ +import { connect } from 'react-redux' +import DappListItem from './DappListItem' +import { + showDownVoteAction, + showUpVoteAction, + fetchVoteRatingAction, +} from '../../../modules/Vote/Vote.reducer' +import { toggleProfileModalAction } from '../../../modules/Profile/Profile.reducer' + +const mapDispatchToProps = dispatch => ({ + onClickUpVote: () => dispatch(showUpVoteAction()), + onClickDownVote: () => dispatch(showDownVoteAction()), + onClickUpVote: dapp => { + dispatch(showUpVoteAction(dapp)) + dispatch(fetchVoteRatingAction(dapp, true, 0)) + }, + onClickDownVote: dapp => { + dispatch(showDownVoteAction(dapp)) + dispatch(fetchVoteRatingAction(dapp, false, 3244)) + }, + onToggleProfileModal: data => dispatch(toggleProfileModalAction(data)), +}) + +export default connect( + null, + mapDispatchToProps, +)(DappListItem) diff --git a/src/common/components/DappListItem/DappListItem.jsx b/src/common/components/DappListItem/DappListItem.jsx new file mode 100644 index 0000000..65a2694 --- /dev/null +++ b/src/common/components/DappListItem/DappListItem.jsx @@ -0,0 +1,98 @@ +import React from 'react' +import PropTypes from 'prop-types' +import ReactImageFallback from 'react-image-fallback' +import { DappModel } from '../../utils/models' +import styles from './DappListItem.module.scss' +import icon from '../../assets/images/icon.svg' +import sntIcon from '../../assets/images/SNT.svg' +import upvoteArrowIcon from '../../assets/images/upvote-arrow.svg' +import downvoteArrowIcon from '../../assets/images/downvote-arrow.svg' + +const DappListItem = props => { + const { + dapp, + onClickUpVote, + onClickDownVote, + isRanked, + position, + category, + showActionButtons, + onToggleProfileModal, + } = props + + const { name, description, url, image } = dapp + + const handleUpVote = () => { + onClickUpVote(dapp) + } + + const handleDownVote = () => { + onClickDownVote(dapp) + } + + return ( +
+ {isRanked &&
{position}
} +
onToggleProfileModal(name)} + > + +
+
+
onToggleProfileModal(name)}> +

{name}

+

+ {description} +

+
+ + {url} +  → + + {showActionButtons && ( +
+ + SNT + {dapp.sntValue} + +
+ + + Upvote + + + + Downvote + +
+
+ )} +
+
+ ) +} + +DappListItem.defaultProps = { + isRanked: false, + showActionButtons: false, +} + +DappListItem.propTypes = { + dapp: PropTypes.shape(DappModel).isRequired, + isRanked: PropTypes.bool, + showActionButtons: PropTypes.bool, + position: PropTypes.number.isRequired, + onClickUpVote: PropTypes.func.isRequired, + onClickDownVote: PropTypes.func.isRequired, +} + +export default DappListItem diff --git a/src/common/components/DappListItem/DappListItem.module.scss b/src/common/components/DappListItem/DappListItem.module.scss new file mode 100644 index 0000000..68bd085 --- /dev/null +++ b/src/common/components/DappListItem/DappListItem.module.scss @@ -0,0 +1,129 @@ +@import '../../styles/variables'; + +.listItem { + font-family: $font; + background: $background; + display: flex; + height: calculateRem(145); + margin: 0 calculateRem(16) calculateRem(11) calculateRem(16); + position: relative; +} + +.rankedListItem { + font-family: $font; + background: $background; + display: flex; + margin: 0 0 calculateRem(11) 0.5rem; + height: calculateRem(145); + position: relative; +} + +.column { + display: flex; + flex-direction: column; + flex: 1 1 auto; +} + +.header { + color: $headline-color; + font-size: calculateRem(15); + line-height: calculateRem(22); + margin-bottom: calculateRem(2); + margin-top: calculateRem(12); + font-weight: 500; + cursor: pointer; +} + +.image { + max-width: calculateRem(40); + max-height: calculateRem(40); + margin-top: calculateRem(15); + margin-right: calculateRem(16); + border-radius: 50%; + cursor: pointer; +} + +.url { + font-size: calculateRem(12); + color: $link-color; + text-decoration: none; + margin-bottom: 12px; +} + +.description { + color: $text-color; + font-size: calculateRem(13); + line-height: calculateRem(18); + margin-bottom: calculateRem(2); + margin-top: 0; + max-height: calculateRem(40); + overflow-y: hidden; + cursor: pointer; + + display: -webkit-box; + text-overflow: ellipsis; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; +} + +.position { + flex: 0 0 auto; + margin-top: calculateRem(20); + margin-right: calculateRem(16); + font-size: calculateRem(13); +} + +.imgWrapper { + flex: 0 0 auto; +} + +.sntAmount { + font-size: calculateRem(12); + font-weight: 500; + // width: 80px; + display: inline-block; + margin-right: calculateRem(12); +} + +.sntAmount img { + vertical-align: middle; + margin-right: calculateRem(6); +} + +.vote { + font-size: calculateRem(11); + text-transform: uppercase; + color: $link-color; + font-weight: 600; + text-decoration: none; + // width: calculateRem(80); + display: inline-block; + cursor: pointer; +} + +.vote:not(:last-child) { + margin-right: calculateRem(12); +} + +.vote img { + vertical-align: middle; + margin-right: calculateRem(2); +} + +.actionArea { + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + margin-top: auto; + + .voteTriggers { + display: inherit; + align-items: inherit; + } +} + +.actionArea > * { + margin-bottom: calculateRem(8); +} diff --git a/src/common/components/DappListItem/index.js b/src/common/components/DappListItem/index.js new file mode 100644 index 0000000..6c93fda --- /dev/null +++ b/src/common/components/DappListItem/index.js @@ -0,0 +1,3 @@ +import DappListItem from './DappListItem.container' + +export default DappListItem diff --git a/src/common/components/FeatureDapps/FeatureDapps.jsx b/src/common/components/FeatureDapps/FeatureDapps.jsx new file mode 100644 index 0000000..9fa6057 --- /dev/null +++ b/src/common/components/FeatureDapps/FeatureDapps.jsx @@ -0,0 +1,42 @@ +import React from 'react' +import ReactImageFallback from 'react-image-fallback' +import styles from './FeatureDapps.module.scss' +import fallbackBanner from '../../assets/images/fallback.svg' +import icon from '../../assets/images/icon.svg' + +const FeatureDapps = props => { + return ( + <> +
+ {props.featured.map((dapp, index) => ( + +
+ +
+
+ +
+

{dapp.name}

+ + {dapp.description} + +
+
+
+ ))} +
+ + ) +} + +export default FeatureDapps diff --git a/src/common/components/FeatureDapps/FeatureDapps.module.scss b/src/common/components/FeatureDapps/FeatureDapps.module.scss new file mode 100644 index 0000000..1819a15 --- /dev/null +++ b/src/common/components/FeatureDapps/FeatureDapps.module.scss @@ -0,0 +1,77 @@ +@import '../../../common/styles/variables'; + +.grid { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + grid-template-rows: unset; + position: relative; + margin: 0 calculateRem(10) calculateRem(30) calculateRem(10); + overflow-x: scroll; + overflow-y: hidden; + -webkit-overflow-scrolling: touch; + + @media (min-width: $desktop) { + grid-template-columns: 1fr 1fr 1fr; + grid-template-rows: unset; + overflow-x: hidden; + } +} + +.dapp { + min-width: 220px; + font-family: $font; + background: $background; + display: flex; + flex-direction: column; + margin: 0 calculateRem(20) calculateRem(20) calculateRem(20); + text-decoration: none; +} + +.bannerWrapper { + width: 100%; + height: 0; + position: relative; + padding-bottom: 48%; + + .banner { + width: 100%; + height: 100%; + position: absolute; + left: 0; + top: 0; + border-radius: 20px; + object-fit: cover; + } +} + +.dapp_details { + display: flex; + flex-direction: row; +} + +.dapp_details__image { + max-width: calculateRem(40); + max-height: calculateRem(40); + margin-top: calculateRem(10); + margin-right: calculateRem(16); + border-radius: 50%; +} + +.dapp_details__header { + color: $headline-color; + font-size: calculateRem(15); + line-height: calculateRem(22); + margin-bottom: calculateRem(2); + margin-top: calculateRem(12); + font-weight: 500; +} + +.dapp_details__description { + color: $text-color; + font-size: calculateRem(13); + line-height: calculateRem(18); + margin-bottom: calculateRem(2); + margin-top: 0; + max-height: calculateRem(40); + overflow-y: hidden; +} diff --git a/src/common/components/FeatureDapps/index.js b/src/common/components/FeatureDapps/index.js new file mode 100644 index 0000000..6a15461 --- /dev/null +++ b/src/common/components/FeatureDapps/index.js @@ -0,0 +1,3 @@ +import FeatureDapps from './FeatureDapps' + +export default FeatureDapps diff --git a/src/common/components/Modal/Modal.jsx b/src/common/components/Modal/Modal.jsx new file mode 100644 index 0000000..87bf100 --- /dev/null +++ b/src/common/components/Modal/Modal.jsx @@ -0,0 +1,50 @@ +import React from 'react' +import PropTypes from 'prop-types' +import styles from './Modal.module.scss' + +const Modal = props => { + const { + visible, + children, + modalClassName, + windowClassName, + contentClassName, + onClickClose, + } = props + + return ( +
+
+
+ + +
+
{visible && children}
+
+
+ ) +} + +Modal.defaultProps = { + modalClassName: '', + windowClassName: '', + contentClassName: '', + children: null, +} + +Modal.propTypes = { + visible: PropTypes.bool.isRequired, + modalClassName: PropTypes.string, + windowClassName: PropTypes.string, + contentClassName: PropTypes.string, + children: PropTypes.oneOfType([ + PropTypes.arrayOf(PropTypes.node), + PropTypes.node, + ]), + onClickClose: PropTypes.func.isRequired, +} + +export default Modal diff --git a/src/common/components/Modal/Modal.module.scss b/src/common/components/Modal/Modal.module.scss new file mode 100644 index 0000000..7ac1a62 --- /dev/null +++ b/src/common/components/Modal/Modal.module.scss @@ -0,0 +1,67 @@ +@import '../../styles/variables'; + +.wrapper { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + position: fixed; + left: 0; + top: 0; + pointer-events: none; + opacity: 0; + background: rgba(255, 255, 255, 0.5); + z-index: 4096; + + transition-property: opacity; + transition-duration: 0.25s; + + .window { + width: 100%; + line-height: normal; + position: relative; + border-radius: 16px; + background: #fff; + box-shadow: 0px 2px 16px rgba(0, 9, 26, 0.12); + overflow-x: hidden; + overflow-y: auto; + } + + .close { + width: 24px; + height: 24px; + display: none; + align-items: center; + justify-content: center; + position: absolute; + right: 8px; + top: 8px; + color: #fff; + font-family: 'Times New Roman', Times, serif !important; + font-size: 22px; + font-weight: 700; + border-radius: 50%; + background: #939ba1; + cursor: pointer; + transform: rotate(45deg); + } +} + +.wrapper.active { + pointer-events: auto; + opacity: 1; +} + +@media (min-width: $desktop) { + .wrapper { + .window { + width: 400px; + max-height: 90%; + } + + .close { + display: flex; + } + } +} diff --git a/src/common/components/Modal/index.js b/src/common/components/Modal/index.js new file mode 100644 index 0000000..498702f --- /dev/null +++ b/src/common/components/Modal/index.js @@ -0,0 +1,3 @@ +import Modal from './Modal' + +export default Modal diff --git a/src/common/components/Slider/Slider.jsx b/src/common/components/Slider/Slider.jsx new file mode 100644 index 0000000..99bcc2b --- /dev/null +++ b/src/common/components/Slider/Slider.jsx @@ -0,0 +1,24 @@ +import React from 'react' +import PropTypes from 'prop-types' +import RCSlider from 'rc-slider' + +import './Slider.scss' + +const Slider = props => { + const { min, max, value, onChange } = props + + return ( +
+ +
+ ) +} + +Slider.propTypes = { + min: PropTypes.number.isRequired, + max: PropTypes.number.isRequired, + value: PropTypes.number.isRequired, + onChange: PropTypes.func.isRequired, +} + +export default Slider diff --git a/src/common/components/Slider/Slider.scss b/src/common/components/Slider/Slider.scss new file mode 100644 index 0000000..6e9b596 --- /dev/null +++ b/src/common/components/Slider/Slider.scss @@ -0,0 +1,28 @@ +@import '../../../common/styles/variables'; + +.slider { + margin-top: -3px; +} + +.slider .rc-slider-rail { + height: 8px; +} + +.slider .rc-slider-track { + height: 8px; + background-color: $link-color; +} + +.slider .rc-slider-handle { + width: 28px; + height: 28px; + margin-top: -9px; + margin-left: -13px; + border: 0; + background: $link-color; +} + +.slider .rc-slider-handle:hover, +.slider .rc-slider-handle:active { + box-shadow: 0 0 5px rgba(0, 0, 0, 0.48); +} diff --git a/src/common/components/Slider/index.js b/src/common/components/Slider/index.js new file mode 100644 index 0000000..0be37e4 --- /dev/null +++ b/src/common/components/Slider/index.js @@ -0,0 +1,3 @@ +import Slider from './Slider' + +export default Slider diff --git a/src/common/components/ViewAll/ViewAll.jsx b/src/common/components/ViewAll/ViewAll.jsx new file mode 100644 index 0000000..e375c85 --- /dev/null +++ b/src/common/components/ViewAll/ViewAll.jsx @@ -0,0 +1,20 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { Link } from 'react-router-dom' +import styles from './ViewAll.module.scss' + +const ViewAll = props => { + const { size } = props + + return ( + + View all → + + ) +} + +ViewAll.propTypes = { + size: PropTypes.string.isRequired, +} + +export default ViewAll diff --git a/src/common/components/ViewAll/ViewAll.module.scss b/src/common/components/ViewAll/ViewAll.module.scss new file mode 100644 index 0000000..764f4e5 --- /dev/null +++ b/src/common/components/ViewAll/ViewAll.module.scss @@ -0,0 +1,15 @@ +@import '../../styles/variables'; + +.url { + font-family: $font; + color: $link-color; + text-decoration: none; +} + +.small { + font-size: calculateRem(13); +} + +.large { + font-size: calculateRem(15); +} diff --git a/src/common/components/ViewAll/index.js b/src/common/components/ViewAll/index.js new file mode 100644 index 0000000..3ccf174 --- /dev/null +++ b/src/common/components/ViewAll/index.js @@ -0,0 +1,3 @@ +import ViewAll from './ViewAll' + +export default ViewAll diff --git a/src/common/data/alert.js b/src/common/data/alert.js new file mode 100644 index 0000000..c887caa --- /dev/null +++ b/src/common/data/alert.js @@ -0,0 +1,10 @@ +const alert = { + visible: false, + msg: '', + positiveLabel: '', + negativeLabel: '', + positiveListener: null, + negativeListener: null, +} + +export default alert diff --git a/src/common/data/categories.js b/src/common/data/categories.js new file mode 100644 index 0000000..26fc94e --- /dev/null +++ b/src/common/data/categories.js @@ -0,0 +1,7 @@ +export const EXCHANGES = 'EXCHANGES' +export const MARKETPLACES = 'MARKETPLACES' +export const COLLECTIBLES = 'COLLECTIBLES' +export const GAMES = 'GAMES' +export const SOCIAL_NETWORKS = 'SOCIAL_NETWORKS' +export const UTILITIES = 'UTILITIES' +export const OTHER = 'OTHER' diff --git a/src/common/data/dapps.js b/src/common/data/dapps.js new file mode 100644 index 0000000..c2323a5 --- /dev/null +++ b/src/common/data/dapps.js @@ -0,0 +1,633 @@ +import * as Categories from './categories' + +const Dapps = [ + { + metadata: { + name: 'Airswap', + url: 'https://instant.airswap.io/', + image: '/images/dapps/airswap.png', + description: 'Meet the future of trading', + category: Categories.EXCHANGES, + dateAdded: '2019-05-05', + categoryPosition: 13, + }, + rate: 45, + }, + { + metadata: { + name: 'Bancor', + url: 'https://www.bancor.network/', + image: '/images/dapps/bancor.png', + description: 'Bancor is a decentralized liquidity network', + category: Categories.EXCHANGES, + dateAdded: '2019-03-05', + categoryPosition: 12, + }, + rate: 345, + }, + { + metadata: { + name: 'Kyber', + url: 'https://web3.kyber.network', + description: + 'On-chain, instant and liquid platform for exchange and payment', + image: '/images/dapps/kyber.png', + category: Categories.EXCHANGES, + dateAdded: '2019-04-05', + categoryPosition: 11, + }, + rate: 2345, + }, + { + metadata: { + name: 'Uniswap', + url: 'https://uniswap.exchange/', + description: + 'Seamlessly exchange ERC20 tokens, or use a formalized model to pool liquidity reserves', + image: '/images/dapps/uniswap.png', + category: Categories.EXCHANGES, + dateAdded: '2019-04-23', + categoryPosition: 10, + }, + rate: 12345, + }, + { + metadata: { + name: 'DAI by MakerDao', + url: 'https://dai.makerdao.com', + description: 'Stability for the blockchain', + image: '/images/dapps/dai.png', + category: Categories.EXCHANGES, + dateAdded: '2019-04-05', + categoryPosition: 9, + }, + rate: 22345, + }, + { + metadata: { + name: 'Augur', + url: 'https://augur.net', + description: + 'A prediction market protocol owned and operated by the people that use it', + image: '/images/dapps/augur.svg', + category: Categories.EXCHANGES, + dateAdded: '2019-04-11', + categoryPosition: 8, + }, + rate: 32345, + }, + { + metadata: { + name: 'LocalEthereum', + url: 'https://localethereum.com/', + description: 'The smartest way to buy and sell Ether', + image: '/images/dapps/local-ethereum.png', + category: Categories.EXCHANGES, + dateAdded: '2019-04-05', + categoryPosition: 7, + }, + rate: 42345, + }, + { + metadata: { + name: 'Eth2phone', + url: 'https://eth2.io', + description: 'Send Ether by phone number', + image: '/images/dapps/eth2phone.png', + category: Categories.EXCHANGES, + dateAdded: '2019-04-05', + categoryPosition: 6, + }, + rate: 52345, + }, + { + metadata: { + name: 'DDEX', + url: 'https://ddex.io/', + description: + 'Instant, real-time order matching with secure on-chain settlement', + image: '/images/dapps/ddex.png', + category: Categories.EXCHANGES, + dateAdded: '2019-04-05', + categoryPosition: 5, + }, + rate: 62345, + }, + { + metadata: { + name: 'Nuo', + url: 'https://app.nuo.network/lend/', + description: + 'The non-custodial way to lend, borrow or margin trade cryptocurrency', + image: '/images/dapps/nuo.png', + category: Categories.EXCHANGES, + dateAdded: '2019-04-05', + categoryPosition: 4, + }, + rate: 72345, + }, + { + metadata: { + name: 'EasyTrade', + url: 'https://easytrade.io', + description: 'One exchange for every token', + image: '/images/dapps/easytrade.png', + category: Categories.EXCHANGES, + dateAdded: '2019-04-05', + categoryPosition: 3, + }, + rate: 82345, + }, + { + metadata: { + name: 'slow.trade', + url: 'https://slow.trade/', + description: + 'Trade fairly priced crypto assets on the first platform built with the DutchX protocol', + image: '/images/dapps/slowtrade.png', + category: Categories.EXCHANGES, + dateAdded: '2019-04-05', + categoryPosition: 2, + }, + rate: 92345, + }, + { + metadata: { + name: 'Expo Trading', + url: 'https://expotrading.com/trade/', + description: 'The simplest way to margin trade cryptocurrency', + image: '/images/dapps/expotrading.png', + category: Categories.EXCHANGES, + dateAdded: '2019-04-11', + categoryPosition: 1, + }, + rate: 102345, + }, + { + metadata: { + name: 'Bidali', + url: 'https://commerce.bidali.com/dapp', + description: 'Buy from top brands with crypto', + image: '/images/dapps/bidali.png', + category: Categories.MARKETPLACES, + dateAdded: '2019-05-01', + }, + rate: 10246, + }, + { + metadata: { + name: 'blockimmo', + url: 'https://blockimmo.ch', + description: + 'blockimmo is a blockchain powered, regulated platform enabling shared property investments and ownership', + image: '/images/dapps/blockimmo.png', + category: Categories.MARKETPLACES, + dateAdded: '2019-04-05', + categoryPosition: 2, + }, + rate: 12345, + }, + { + metadata: { + name: 'CryptoCribs', + url: 'https://cryptocribs.com', + description: 'Travel the globe. Pay in crypto', + image: '/images/dapps/cryptocribs.png', + category: Categories.MARKETPLACES, + dateAdded: '2019-04-05', + categoryPosition: 2, + }, + rate: 12345, + }, + { + metadata: { + name: 'Ethlance', + url: 'https://ethlance.com', + description: + 'The future of work is now. Hire people or work yourself in return for ETH', + image: '/images/dapps/ethlance.png', + category: Categories.MARKETPLACES, + dateAdded: '2019-04-05', + categoryPosition: 2, + }, + rate: 12345, + }, + { + metadata: { + name: 'OpenSea', + url: 'https://opensea.io', + description: 'The largest decentralized marketplace for cryptogoods', + image: '/images/dapps/opensea.png', + category: Categories.MARKETPLACES, + dateAdded: '2019-04-05', + categoryPosition: 2, + }, + rate: 12345, + }, + { + metadata: { + name: 'KnownOrigin', + url: 'https://dapp.knownorigin.io/gallery', + description: 'Discover, buy and collect digital artwork', + image: '/images/dapps/knownorigin.png', + category: Categories.MARKETPLACES, + dateAdded: '2019-04-11', + categoryPosition: 2, + }, + rate: 12345, + }, + { + metadata: { + name: 'dBay', + url: 'https://dbay.ai', + description: 'Buy from all your favorite DApps in one place', + image: '/images/dapps/dBay.png', + category: Categories.MARKETPLACES, + dateAdded: '2019-04-23', + categoryPosition: 2, + }, + rate: 12345, + }, + { + metadata: { + name: 'Name Bazaar', + url: 'https://namebazaar.io', + description: 'ENS name marketplace', + image: '/images/dapps/name-bazaar.png', + category: Categories.MARKETPLACES, + dateAdded: '2019-04-05', + categoryPosition: 2, + }, + rate: 12345, + }, + { + metadata: { + name: 'The Bounties Network', + url: 'https://bounties.network/', + description: 'Bounties on any task, paid in any token', + image: '/images/dapps/bounties-network.png', + category: Categories.MARKETPLACES, + dateAdded: '2019-04-05', + categoryPosition: 2, + }, + rate: 12345, + }, + { + metadata: { + name: 'Emoon', + url: 'https://www.emoon.io/', + description: + 'A decentralized marketplace for buying & selling crypto assets', + image: '/images/dapps/emoon.png', + category: Categories.MARKETPLACES, + dateAdded: '2019-04-05', + categoryPosition: 2, + }, + rate: 12345, + }, + { + metadata: { + name: 'Astro Ledger', + url: 'https://www.astroledger.org/#/onSale', + description: 'Funding space grants with blockchain star naming', + image: '/images/dapps/astroledger.svg', + category: Categories.MARKETPLACES, + dateAdded: '2019-04-11', + categoryPosition: 2, + }, + rate: 12345, + }, + { + metadata: { + name: 'SuperRare', + url: 'https://superrare.co/market', + description: + 'Buy, sell and collect unique digital creations by artists around the world', + image: '/images/dapps/superrare.png', + category: Categories.MARKETPLACES, + dateAdded: '2019-04-05', + categoryPosition: 2, + }, + rate: 12345, + }, + { + metadata: { + name: 'CryptoCare', + url: 'https://cryptocare.tech', + description: + 'Give your Ether some heart! Collectibles that make the world a better place', + image: '/images/dapps/cryptocare.jpg', + category: Categories.COLLECTIBLES, + dateAdded: '2019-04-11', + categoryPosition: 2, + }, + rate: 12345, + }, + { + metadata: { + name: 'CryptoKitties', + url: 'https://www.cryptokitties.co', + description: 'Collect and breed adorable digital cats', + image: '/images/dapps/cryptokitties.png', + category: Categories.COLLECTIBLES, + dateAdded: '2019-04-05', + categoryPosition: 2, + }, + rate: 12345, + }, + { + metadata: { + name: 'Cryptographics', + url: 'https://cryptographics.app/', + description: + 'A digital art hub for creation, trading, and collecting unique items', + image: '/images/dapps/cryptographics.png', + category: Categories.COLLECTIBLES, + dateAdded: '2019-04-05', + categoryPosition: 2, + }, + rate: 12345, + }, + { + metadata: { + name: 'CryptoPunks', + url: 'https://www.larvalabs.com/cryptopunks', + description: '10,000 unique collectible punks', + image: '/images/dapps/cryptopunks.png', + category: Categories.COLLECTIBLES, + dateAdded: '2019-04-05', + categoryPosition: 2, + }, + rate: 12345, + }, + { + metadata: { + name: 'Crypto Takeovers', + url: 'https://cryptotakeovers.com/', + description: 'Predict and conquer the world. Make a crypto fortune', + image: '/images/dapps/cryptotakeovers.png', + category: Categories.GAMES, + dateAdded: '2019-04-05', + categoryPosition: 2, + }, + rate: 12345, + }, + { + metadata: { + name: 'CryptoFighters', + url: 'https://cryptofighters.io', + description: 'Collect train and fight digital fighters', + image: '/images/dapps/cryptofighters.png', + category: Categories.GAMES, + dateAdded: '2019-04-11', + categoryPosition: 2, + }, + rate: 12345, + }, + { + metadata: { + name: 'Decentraland', + url: 'https://market.decentraland.org/', + description: + 'A virtual reality platform powered by the Ethereum blockchain', + image: '/images/dapps/decentraland.png', + category: Categories.GAMES, + dateAdded: '2019-04-05', + categoryPosition: 2, + }, + rate: 12345, + }, + { + metadata: { + name: 'Dragonereum', + url: 'https://dapp.dragonereum.io', + description: 'Own and trade dragons, fight with other players', + image: '/images/dapps/dragonereum.png', + category: Categories.GAMES, + dateAdded: '2019-04-05', + categoryPosition: 2, + }, + rate: 12345, + }, + { + metadata: { + name: 'Etherbots', + url: 'https://etherbots.io/', + description: 'Robot wars on Ethereum', + image: '/images/dapps/etherbots.png', + category: Categories.GAMES, + dateAdded: '2019-04-05', + categoryPosition: 2, + }, + rate: 12345, + }, + { + metadata: { + name: 'Etheremon', + url: 'https://www.etheremon.com/', + description: 'Decentralized World of Ether Monsters', + image: '/images/dapps/etheremon.png', + category: Categories.GAMES, + dateAdded: '2019-04-05', + categoryPosition: 2, + }, + rate: 12345, + }, + { + metadata: { + name: 'CryptoStrikers', + url: 'https://www.cryptostrikers.com/', + description: 'The Beautiful (card) Game', + image: '/images/dapps/cryptostrikers.png', + category: Categories.GAMES, + dateAdded: '2019-04-05', + categoryPosition: 2, + }, + rate: 12345, + }, + // { + // metadata: { + // name: 'FairHouse', + // url: 'https://fairhouse.io', + // description: 'Fair and transparent entertainment games.', + // image: '/images/dapps/fairhouse.png', + // category: Categories.GAMES, + // dateAdded: '2019-04-11', + // categoryPosition: 2, + // }, + // rate: 12345, + // }, + { + metadata: { + name: 'Cent', + url: 'https://beta.cent.co/', + description: 'Get wisdom, get money', + image: '/images/dapps/cent.png', + category: Categories.SOCIAL_NETWORKS, + dateAdded: '2019-04-05', + categoryPosition: 2, + }, + rate: 12345, + }, + { + metadata: { + name: 'Kickback', + url: 'https://kickback.events/', + description: + 'Event no shows? No problem. Kickback asks event attendees to put skin in the game with Ethereum', + image: '/images/dapps/kickback.png', + category: Categories.SOCIAL_NETWORKS, + dateAdded: '2019-04-05', + categoryPosition: 2, + }, + rate: 12345, + }, + { + metadata: { + name: 'Peepeth', + url: 'https://peepeth.com/', + description: 'Blockchain-powered microblogging', + image: '/images/dapps/peepeth.png', + category: Categories.SOCIAL_NETWORKS, + dateAdded: '2019-04-05', + categoryPosition: 2, + }, + rate: 12345, + }, + { + metadata: { + name: 'livepeer.tv', + url: 'http://livepeer.tv/', + description: 'Decentralized video broadcasting', + image: '/images/dapps/livepeer.png', + category: Categories.OTHER, + dateAdded: '2019-04-05', + categoryPosition: 2, + }, + rate: 12345, + }, + { + metadata: { + name: 'Aragon', + url: 'https://mainnet.aragon.org/', + description: 'Build unstoppable organizations on Ethereum', + image: '/images/dapps/aragon.png', + category: Categories.UTILITIES, + dateAdded: '2019-04-05', + categoryPosition: 2, + }, + rate: 12345, + }, + { + metadata: { + name: 'Compound Finance', + url: 'https://app.compound.finance/', + description: + 'An open-source protocol for algorithmic, efficient Money Markets on Ethereum', + image: '/images/dapps/compoundfinance.png', + category: Categories.UTILITIES, + dateAdded: '2019-04-11', + categoryPosition: 2, + }, + rate: 12345, + }, + { + metadata: { + name: 'InstaDApp', + url: 'https://instadapp.io/', + description: 'Decentralized Banking', + image: '/images/dapps/instadapp.jpg', + category: Categories.UTILITIES, + dateAdded: '2019-04-11', + categoryPosition: 2, + }, + rate: 12345, + }, + { + metadata: { + name: 'Livepeer', + url: 'https://explorer.livepeer.org/', + description: 'Decentralized video broadcasting', + image: '/images/dapps/livepeer.png', + category: Categories.UTILITIES, + dateAdded: '2019-04-11', + categoryPosition: 2, + }, + rate: 12345, + }, + { + metadata: { + name: 'ETHLend', + url: 'https://app.ethlend.io', + description: 'Decentralized lending on Ethereum', + image: '/images/dapps/ethlend.png', + category: Categories.UTILITIES, + dateAdded: '2019-04-05', + categoryPosition: 2, + }, + rate: 12345, + }, + { + metadata: { + name: 'Civitas', + url: 'https://communities.colu.com/', + description: 'Blockchain-powered local communities', + image: '/images/dapps/civitas.png', + category: Categories.UTILITIES, + dateAdded: '2019-04-11', + categoryPosition: 2, + }, + rate: 12345, + }, + { + metadata: { + name: '3Box', + url: 'https://3box.io/', + description: 'Create and manage your Ethereum Profile', + image: '/images/dapps/3Box.png', + category: Categories.UTILITIES, + dateAdded: '2019-04-11', + categoryPosition: 2, + }, + rate: 12345, + }, + { + metadata: { + name: 'Hexel', + url: 'https://www.onhexel.com/', + description: 'Create your own cryptocurrency', + image: '/images/dapps/hexel.png', + category: Categories.UTILITIES, + dateAdded: '2019-04-11', + categoryPosition: 2, + }, + rate: 12345, + }, + { + metadata: { + name: 'Smartz', + url: 'https://smartz.io', + description: 'Easy smart contract management', + image: '/images/dapps/smartz.png', + category: Categories.UTILITIES, + dateAdded: '2019-04-05', + categoryPosition: 2, + }, + rate: 12345, + }, + { + metadata: { + name: 'SNT Voting DApp', + url: 'https://vote.status.im', + description: + 'Let your SNT be heard! Vote on decisions exclusive to SNT holders, or create a poll of your own.', + image: '/images/dapps/snt-voting.png', + category: Categories.UTILITIES, + dateAdded: '2019-04-05', + categoryPosition: 2, + }, + rate: 12345, + }, +] + +export default Dapps diff --git a/src/common/data/desktop-menu.js b/src/common/data/desktop-menu.js new file mode 100644 index 0000000..daba818 --- /dev/null +++ b/src/common/data/desktop-menu.js @@ -0,0 +1,5 @@ +const desktopMenu = { + visible: false, +} + +export default desktopMenu diff --git a/src/common/data/featured.js b/src/common/data/featured.js new file mode 100644 index 0000000..cc7ac60 --- /dev/null +++ b/src/common/data/featured.js @@ -0,0 +1,32 @@ +import CryptoKittiesBanner from '../assets/images/featured/crytokittes_banner.png' +import CryptoKittiesLogo from '../assets/images/featured/cryptokitties_logo.png' +import AirswapBanner from '../assets/images/featured/airswap_banner.png' +import AirswapLogo from '../assets/images/featured/airswap_logo.png' +import KyberBanner from '../assets/images/featured/kyber_banner.png' +import KyberLogo from '../assets/images/featured/kyber_logo.png' + +const featuredDapps = [ + { + name: 'CryptoKittes', + description: 'Collect and breed adorable digital cats', + url: 'https://cryptokitties.co', + banner: CryptoKittiesBanner, + icon: CryptoKittiesLogo, + }, + { + name: 'Airswap', + description: 'Meet the future of trading', + url: 'https://instant.airswap.io', + banner: AirswapBanner, + icon: AirswapLogo, + }, + { + name: 'Kyber', + description: 'On-chain, instant and liquid exchange and payment', + url: 'https://web3.kyber.network', + banner: KyberBanner, + icon: KyberLogo, + }, +] + +export default featuredDapps diff --git a/src/common/data/how-to-submit.js b/src/common/data/how-to-submit.js new file mode 100644 index 0000000..7e14efb --- /dev/null +++ b/src/common/data/how-to-submit.js @@ -0,0 +1,6 @@ +const howToSubmit = { + visible_how_to_submit: false, + visible_terms: false, +} + +export default howToSubmit diff --git a/src/common/data/profile.js b/src/common/data/profile.js new file mode 100644 index 0000000..f9ea202 --- /dev/null +++ b/src/common/data/profile.js @@ -0,0 +1,6 @@ +const profile = { + visible: false, + dapp: '', +} + +export default profile diff --git a/src/common/data/submit.js b/src/common/data/submit.js new file mode 100644 index 0000000..8a17f3e --- /dev/null +++ b/src/common/data/submit.js @@ -0,0 +1,18 @@ +const submit = { + visible_submit: false, + visible_rating: false, + id: '', + name: '', + desc: '', + url: '', + img: '', + category: '', + imgControl: false, + imgControlZoom: 0, + imgControlMove: false, + imgControlX: 0, + imgControlY: 0, + sntValue: '0', +} + +export default submit diff --git a/src/common/data/transaction-status.js b/src/common/data/transaction-status.js new file mode 100644 index 0000000..554852b --- /dev/null +++ b/src/common/data/transaction-status.js @@ -0,0 +1,3 @@ +import { transactionStatusFetchedInstance } from '../../modules/TransactionStatus/TransactionStatus.utilities' + +export default Object.assign({}, transactionStatusFetchedInstance()) diff --git a/src/common/data/vote.js b/src/common/data/vote.js new file mode 100644 index 0000000..b85d81c --- /dev/null +++ b/src/common/data/vote.js @@ -0,0 +1,9 @@ +const vote = { + visible: false, + dapp: null, + isUpvote: false, + sntValue: '0', + afterVoteRating: null, +} + +export default vote diff --git a/src/common/redux/reducers.js b/src/common/redux/reducers.js new file mode 100644 index 0000000..6eba451 --- /dev/null +++ b/src/common/redux/reducers.js @@ -0,0 +1,25 @@ +import { combineReducers } from 'redux' +import { connectRouter } from 'connected-react-router' +import dapps from '../../modules/Dapps/Dapps.reducer' +// import selectedCategory from '../../modules/CategorySelector/CategorySelector.reducer' +import vote from '../../modules/Vote/Vote.reducer' +import profile from '../../modules/Profile/Profile.reducer' +import submit from '../../modules/Submit/Submit.reducer' +import desktopMenu from '../../modules/DesktopMenu/DesktopMenu.reducer' +import transactionStatus from '../../modules/TransactionStatus/TransactionStatus.recuder' +import alert from '../../modules/Alert/Alert.reducer' +import howToSubmit from '../../modules/HowToSubmit/HowToSubmit.reducer' + +export default history => + combineReducers({ + router: connectRouter(history), + dapps, + // selectedCategory, + vote, + profile, + submit, + desktopMenu, + transactionStatus, + alert, + howToSubmit, + }) diff --git a/src/common/redux/store.js b/src/common/redux/store.js new file mode 100644 index 0000000..d636daa --- /dev/null +++ b/src/common/redux/store.js @@ -0,0 +1,22 @@ +import { compose, createStore, applyMiddleware } from 'redux' +import { routerMiddleware } from 'connected-react-router' +import { createBrowserHistory } from 'history' +import thunk from 'redux-thunk' +import reducer from './reducers' + +export const history = createBrowserHistory({ + basename: process.env.NODE_ENV === 'development' ? '/' : '/discover-dapps/', +}) + +const composeWithDevTools = + /* eslint-disable-next-line no-underscore-dangle */ + window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose + +const configureStore = () => + createStore( + reducer(history), + {}, + composeWithDevTools(applyMiddleware(thunk, routerMiddleware(history))), + ) + +export default configureStore diff --git a/src/common/styles/_base.scss b/src/common/styles/_base.scss new file mode 100644 index 0000000..2768db4 --- /dev/null +++ b/src/common/styles/_base.scss @@ -0,0 +1,351 @@ +/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */ + +/* Document + ========================================================================== */ + +/** + * 1. Correct the line height in all browsers. + * 2. Prevent adjustments of font size after orientation changes in iOS. + */ + +html { + line-height: 1.15; /* 1 */ + -webkit-text-size-adjust: 100%; /* 2 */ +} + +/* Sections + ========================================================================== */ + +/** + * Remove the margin in all browsers. + */ + +body { + margin: 0; +} + +/** + * Render the `main` element consistently in IE. + */ + +main { + display: block; +} + +/** + * Correct the font size and margin on `h1` elements within `section` and + * `article` contexts in Chrome, Firefox, and Safari. + */ + +h1 { + font-size: 2em; + margin: 0.67em 0; +} + +/* Grouping content + ========================================================================== */ + +/** + * 1. Add the correct box sizing in Firefox. + * 2. Show the overflow in Edge and IE. + */ + +hr { + box-sizing: content-box; /* 1 */ + height: 0; /* 1 */ + overflow: visible; /* 2 */ +} + +/** + * 1. Correct the inheritance and scaling of font size in all browsers. + * 2. Correct the odd `em` font sizing in all browsers. + */ + +pre { + font-family: monospace, monospace; /* 1 */ + font-size: 1em; /* 2 */ +} + +/* Text-level semantics + ========================================================================== */ + +/** + * Remove the gray background on active links in IE 10. + */ + +a { + background-color: transparent; +} + +/** + * 1. Remove the bottom border in Chrome 57- + * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. + */ + +abbr[title] { + border-bottom: none; /* 1 */ + text-decoration: underline; /* 2 */ + text-decoration: underline dotted; /* 2 */ +} + +/** + * Add the correct font weight in Chrome, Edge, and Safari. + */ + +b, +strong { + font-weight: bolder; +} + +/** + * 1. Correct the inheritance and scaling of font size in all browsers. + * 2. Correct the odd `em` font sizing in all browsers. + */ + +code, +kbd, +samp { + font-family: monospace, monospace; /* 1 */ + font-size: 1em; /* 2 */ +} + +/** + * Add the correct font size in all browsers. + */ + +small { + font-size: 80%; +} + +/** + * Prevent `sub` and `sup` elements from affecting the line height in + * all browsers. + */ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sub { + bottom: -0.25em; +} + +sup { + top: -0.5em; +} + +/* Embedded content + ========================================================================== */ + +/** + * Remove the border on images inside links in IE 10. + */ + +img { + border-style: none; +} + +/* Forms + ========================================================================== */ + +/** + * 1. Change the font styles in all browsers. + * 2. Remove the margin in Firefox and Safari. + */ + +button, +input, +optgroup, +select, +textarea { + font-family: inherit; /* 1 */ + font-size: 100%; /* 1 */ + line-height: 1.15; /* 1 */ + margin: 0; /* 2 */ +} + +/** + * Show the overflow in IE. + * 1. Show the overflow in Edge. + */ + +button, +input { + /* 1 */ + overflow: visible; +} + +/** + * Remove the inheritance of text transform in Edge, Firefox, and IE. + * 1. Remove the inheritance of text transform in Firefox. + */ + +button, +select { + /* 1 */ + text-transform: none; +} + +/** + * Correct the inability to style clickable types in iOS and Safari. + */ + +button, +[type='button'], +[type='reset'], +[type='submit'] { + -webkit-appearance: button; +} + +/** + * Remove the inner border and padding in Firefox. + */ + +button::-moz-focus-inner, +[type='button']::-moz-focus-inner, +[type='reset']::-moz-focus-inner, +[type='submit']::-moz-focus-inner { + border-style: none; + padding: 0; +} + +/** + * Restore the focus styles unset by the previous rule. + */ + +button:-moz-focusring, +[type='button']:-moz-focusring, +[type='reset']:-moz-focusring, +[type='submit']:-moz-focusring { + outline: 1px dotted ButtonText; +} + +/** + * Correct the padding in Firefox. + */ + +fieldset { + padding: 0.35em 0.75em 0.625em; +} + +/** + * 1. Correct the text wrapping in Edge and IE. + * 2. Correct the color inheritance from `fieldset` elements in IE. + * 3. Remove the padding so developers are not caught out when they zero out + * `fieldset` elements in all browsers. + */ + +legend { + box-sizing: border-box; /* 1 */ + color: inherit; /* 2 */ + display: table; /* 1 */ + max-width: 100%; /* 1 */ + padding: 0; /* 3 */ + white-space: normal; /* 1 */ +} + +/** + * Add the correct vertical alignment in Chrome, Firefox, and Opera. + */ + +progress { + vertical-align: baseline; +} + +/** + * Remove the default vertical scrollbar in IE 10+. + */ + +textarea { + overflow: auto; +} + +/** + * 1. Add the correct box sizing in IE 10. + * 2. Remove the padding in IE 10. + */ + +[type='checkbox'], +[type='radio'] { + box-sizing: border-box; /* 1 */ + padding: 0; /* 2 */ +} + +/** + * Correct the cursor style of increment and decrement buttons in Chrome. + */ + +[type='number']::-webkit-inner-spin-button, +[type='number']::-webkit-outer-spin-button { + height: auto; +} + +/** + * 1. Correct the odd appearance in Chrome and Safari. + * 2. Correct the outline style in Safari. + */ + +[type='search'] { + -webkit-appearance: textfield; /* 1 */ + outline-offset: -2px; /* 2 */ +} + +/** + * Remove the inner padding in Chrome and Safari on macOS. + */ + +[type='search']::-webkit-search-decoration { + -webkit-appearance: none; +} + +/** + * 1. Correct the inability to style clickable types in iOS and Safari. + * 2. Change font properties to `inherit` in Safari. + */ + +::-webkit-file-upload-button { + -webkit-appearance: button; /* 1 */ + font: inherit; /* 2 */ +} + +/* Interactive + ========================================================================== */ + +/* + * Add the correct display in Edge, IE 10+, and Firefox. + */ + +details { + display: block; +} + +/* + * Add the correct display in all browsers. + */ + +summary { + display: list-item; +} + +/* Misc + ========================================================================== */ + +/** + * Add the correct display in IE 10+. + */ + +template { + display: none; +} + +/** + * Add the correct display in IE 10. + */ + +[hidden] { + display: none; +} diff --git a/src/common/styles/_colors.scss b/src/common/styles/_colors.scss new file mode 100644 index 0000000..f2f7f5b --- /dev/null +++ b/src/common/styles/_colors.scss @@ -0,0 +1,47 @@ +$purple: #887af9; +$orange: #fe8f59; +$blue: #51d0f0; +$pink: #d37ef4; +$green: #7cda00; +$red: #fa6565; +$yellow: #ffca0f; + +$purple-bg: rgba( + $color: $purple, + $alpha: 0.15, +); + +$orange-bg: rgba( + $color: $orange, + $alpha: 0.15, +); + +$yellow-bg: rgba( + $color: $yellow, + $alpha: 0.15, +); + +$pink-bg: rgba( + $color: $pink, + $alpha: 0.15, +); + +$blue-bg: rgba( + $color: $blue, + $alpha: 0.15, +); + +$green-bg: rgba( + $color: $green, + $alpha: 0.15, +); + +$red-bg: rgba( + $color: $red, + $alpha: 0.15, +); + +$link-color: #4360df; +$text-color: #939ba1; +$headline-color: #000; +$background: #fff; diff --git a/src/common/styles/_fonts.scss b/src/common/styles/_fonts.scss new file mode 100644 index 0000000..0719be4 --- /dev/null +++ b/src/common/styles/_fonts.scss @@ -0,0 +1,37 @@ +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 400; + src: url('/fonts/Inter-Regular.woff2') format('woff2'), + url('/fonts/Inter-Regular.woff') format('woff'); +} +@font-face { + font-family: 'Inter'; + font-style: italic; + font-weight: 400; + src: url('/fonts/Inter-Italic.woff2') format('woff2'), + url('/fonts/Inter-Italic.woff') format('woff'); +} + +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 500; + src: url('/fonts/Inter-Medium.woff2') format('woff2'), + url('/fonts/Inter-Medium.woff') format('woff'); +} + +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 700; + src: url('/fonts/Inter-Bold.woff2') format('woff2'), + url('/fonts/Inter-Bold.woff') format('woff'); +} +@font-face { + font-family: 'Inter'; + font-style: italic; + font-weight: 700; + src: url('/fonts/Inter-BoldItalic.woff2') format('woff2'), + url('/fonts/Inter-BoldItalic.woff') format('woff'); +} diff --git a/src/common/styles/_functions.scss b/src/common/styles/_functions.scss new file mode 100644 index 0000000..97faa6d --- /dev/null +++ b/src/common/styles/_functions.scss @@ -0,0 +1,3 @@ +@function calculateRem($size) { + @return $size / $base-font-size * 1rem; +} diff --git a/src/common/styles/_variables.scss b/src/common/styles/_variables.scss new file mode 100644 index 0000000..a9b23fe --- /dev/null +++ b/src/common/styles/_variables.scss @@ -0,0 +1,9 @@ +@import 'base'; +@import 'colors'; +@import 'fonts'; + +$font: 'Inter'; +$base-font-size: 16; +$desktop: 830px; + +@import 'functions'; diff --git a/src/common/utils/categories.js b/src/common/utils/categories.js new file mode 100644 index 0000000..b109ebc --- /dev/null +++ b/src/common/utils/categories.js @@ -0,0 +1,7 @@ +import * as Categories from '../data/categories' +import humanise from './humanise' + +export default Object.entries(Categories).map(entry => ({ + key: entry[1], + value: humanise(entry[1]), +})) diff --git a/src/common/utils/categories.test.js b/src/common/utils/categories.test.js new file mode 100644 index 0000000..3b376b3 --- /dev/null +++ b/src/common/utils/categories.test.js @@ -0,0 +1,36 @@ +import categories from './categories' + +describe('categories', () => { + test('it should return the correct data structure of categories', () => { + expect(categories).toEqual([ + { + key: 'EXCHANGES', + value: 'Exchanges', + }, + { + key: 'MARKETPLACES', + value: 'Marketplaces', + }, + { + key: 'COLLECTIBLES', + value: 'Collectibles', + }, + { + key: 'GAMES', + value: 'Games', + }, + { + key: 'SOCIAL_NETWORKS', + value: 'Social Networks', + }, + { + key: 'UTILITIES', + value: 'Utilities', + }, + { + key: 'OTHER', + value: 'Other', + }, + ]) + }) +}) diff --git a/src/common/utils/humanise.js b/src/common/utils/humanise.js new file mode 100644 index 0000000..f8653cc --- /dev/null +++ b/src/common/utils/humanise.js @@ -0,0 +1,12 @@ +const humanise = (value, joiner = ' ') => { + if (!value) { + return '' + } + + return value + .split('_') + .map(word => `${word[0]}${word.slice(1).toLowerCase()}`) + .join(joiner) +} + +export default humanise diff --git a/src/common/utils/humanise.test.js b/src/common/utils/humanise.test.js new file mode 100644 index 0000000..b233e66 --- /dev/null +++ b/src/common/utils/humanise.test.js @@ -0,0 +1,19 @@ +import humanise from './humanise' + +describe('humanise', () => { + test('it should transform a constant string for human reading', () => { + const first = 'TEST' + const second = 'ANOTHER_TEST' + + expect(humanise(first)).toEqual('Test') + expect(humanise(second)).toEqual('Another Test') + }) + + test('it should handle being passed a null', () => { + expect(humanise(null)).toEqual('') + }) + + test('it should handle being passed undefined', () => { + expect(humanise()).toEqual('') + }) +}) diff --git a/src/common/utils/models.js b/src/common/utils/models.js new file mode 100644 index 0000000..b4cd760 --- /dev/null +++ b/src/common/utils/models.js @@ -0,0 +1,14 @@ +import PropTypes from 'prop-types' + +export const DappModel = { + name: PropTypes.string, + url: PropTypes.string, + image: PropTypes.string, + description: PropTypes.string, + category: PropTypes.string, + dateAdded: PropTypes.string, + sntValue: PropTypes.number, + categoryPosition: PropTypes.number, +} + +export const DappListModel = PropTypes.arrayOf(PropTypes.shape(DappModel)) diff --git a/src/common/utils/reducer.js b/src/common/utils/reducer.js new file mode 100644 index 0000000..e0c165b --- /dev/null +++ b/src/common/utils/reducer.js @@ -0,0 +1,11 @@ +export default (map, defaultState) => (currentState, action) => { + const state = !currentState ? defaultState : currentState + + if (!action) { + return state + } + + return Object.keys(map).includes(action.type) + ? map[action.type](state, action.payload) + : state +} diff --git a/src/common/utils/reducer.test.js b/src/common/utils/reducer.test.js new file mode 100644 index 0000000..ef5d06b --- /dev/null +++ b/src/common/utils/reducer.test.js @@ -0,0 +1,50 @@ +import util from './reducer' + +describe('reducer utility', () => { + const reducers = { + TEST_ACTION: (state, payload) => ({ + ...state, + ...payload, + }), + } + + const state = { foo: 'bar' } + const action = { type: 'TEST_ACTION', payload: { baz: true } } + const missingAction = { type: 'MISSING_ACTION' } + const reducer = util(reducers, state) + + test("it should return the existing state when the action doesn't exist", () => { + // Given an existing state + // And an action we can't reduce + + // We should expect the function to pass through the existing state + expect(reducer(state, missingAction)).toEqual({ foo: 'bar' }) + }) + + test('it should call the correct reducer when the action exists', () => { + // Given an existing state + // And an action we can reduce + + // We expect the function to return the correctly reduced new state + expect(reducer(state, action)).toEqual({ + foo: 'bar', + baz: true, + }) + }) + + test('it should return the default state if the existing state is null', () => { + // Given a null state + // And an action we can't reduce + + // We expect the default state + expect(reducer(null, missingAction)).toEqual({ foo: 'bar' }) + }) + + test('it should return the existing state if the action is null', () => { + // Given a null state + // And an action that is undefined + + // We expect the default state + expect(reducer(state, undefined)).toEqual({ foo: 'bar' }) + }) +}) diff --git a/src/index.jsx b/src/index.jsx new file mode 100644 index 0000000..1c2dbb3 --- /dev/null +++ b/src/index.jsx @@ -0,0 +1,17 @@ +import React from 'react' +import ReactDOM from 'react-dom' +import { Provider } from 'react-redux' +import { ConnectedRouter } from 'connected-react-router' +import App from './modules/App' +import configureStore, { history } from './common/redux/store' + +const store = configureStore() + +ReactDOM.render( + + + + + , + document.getElementById('root'), +) diff --git a/src/modules/Alert/Alert.container.js b/src/modules/Alert/Alert.container.js new file mode 100644 index 0000000..cf9eb76 --- /dev/null +++ b/src/modules/Alert/Alert.container.js @@ -0,0 +1,13 @@ +import { connect } from 'react-redux' +import Alert from './Alert' +import { hideAlertAction } from './Alert.reducer' + +const mapStateToProps = state => state.alert +const mapDispatchToProps = dispatch => ({ + hideAlert: () => dispatch(hideAlertAction()), +}) + +export default connect( + mapStateToProps, + mapDispatchToProps, +)(Alert) diff --git a/src/modules/Alert/Alert.jsx b/src/modules/Alert/Alert.jsx new file mode 100644 index 0000000..e6e9570 --- /dev/null +++ b/src/modules/Alert/Alert.jsx @@ -0,0 +1,61 @@ +import React from 'react' +import PropTypes from 'prop-types' +import styles from './Alert.module.scss' + +class Alert extends React.Component { + constructor(props) { + super(props) + this.onClickPositive = this.onClickPositive.bind(this) + this.onClickNegative = this.onClickNegative.bind(this) + } + onClickPositive() { + const { hideAlert, positiveListener } = this.props + hideAlert() + if (positiveListener !== null) positiveListener() + } + onClickNegative() { + const { hideAlert, negativeListener } = this.props + hideAlert() + if (negativeListener !== null) negativeListener() + } + render() { + const { visible, msg, positiveLabel, negativeLabel } = this.props + const cssClassActive = visible ? styles.active : '' + + return ( +
+
+
{msg}
+
+
+ {positiveLabel} +
+ {negativeLabel !== '' && ( +
+ {negativeLabel} +
+ )} +
+
+
+ ) + } +} + +Alert.defaultProps = { + negativeLabel: '', + positiveListener: null, + negativeListener: null, +} + +Alert.propTypes = { + visible: PropTypes.bool.isRequired, + msg: PropTypes.string.isRequired, + positiveLabel: PropTypes.string.isRequired, + negativeLabel: PropTypes.string, + positiveListener: PropTypes.func, + negativeListener: PropTypes.func, + hideAlert: PropTypes.func.isRequired, +} + +export default Alert diff --git a/src/modules/Alert/Alert.module.scss b/src/modules/Alert/Alert.module.scss new file mode 100644 index 0000000..b96df00 --- /dev/null +++ b/src/modules/Alert/Alert.module.scss @@ -0,0 +1,66 @@ +@import '../../common/styles/variables'; + +.alertWrapper { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + position: fixed; + left: 0; + top: 0; + font-family: $font; + font-size: 16px; + background: rgba(255, 255, 255, 0.5); + opacity: 0; + z-index: 4098; + pointer-events: none; + + transition-property: opacity; + transition-duration: 0.25s; + + .alert { + width: 280px; + max-width: 90%; + box-sizing: border-box; + font-weight: 400; + padding: 16px; + border-radius: 4px; + opacity: 0; + background: #fff; + border-radius: 8px; + box-shadow: 0px 2px 16px rgba(0, 9, 26, 0.12); + + .msg { + max-height: 384px; + text-align: left; + margin-bottom: 16px; + overflow: auto; + } + + .actions { + display: flex; + justify-content: flex-end; + } + + .textButton { + color: $link-color; + text-transform: uppercase; + text-decoration: none !important; + font-weight: 700; + font-size: 14px; + border-radius: 4px; + padding: 4px 12px; + cursor: pointer; + } + } +} + +.alertWrapper.active { + opacity: 1; + pointer-events: auto; + + .alert { + opacity: 1; + } +} diff --git a/src/modules/Alert/Alert.reducer.js b/src/modules/Alert/Alert.reducer.js new file mode 100644 index 0000000..0e76174 --- /dev/null +++ b/src/modules/Alert/Alert.reducer.js @@ -0,0 +1,59 @@ +import alertInitialState from '../../common/data/alert' +import reducerUtil from '../../common/utils/reducer' + +const SHOW_ALERT = 'SHOW_ALERT' +const HIDE_ALERT = 'HIDE_ALERT' + +export const showAlertAction = ( + msg, + positiveLabel, + negativeLabel, + positiveListener, + negativeListener, +) => ({ + type: SHOW_ALERT, + payload: { + msg, + positiveLabel, + negativeLabel, + positiveListener, + negativeListener, + }, +}) + +export const hideAlertAction = () => ({ + type: HIDE_ALERT, + payload: null, +}) + +const showAlert = (state, payload) => { + const { + msg, + positiveLabel, + negativeLabel, + positiveListener, + negativeListener, + } = payload + + return Object.assign({}, state, { + visible: true, + msg, + positiveLabel: positiveLabel !== undefined ? positiveLabel : 'OK', + negativeLabel, + positiveListener, + negativeListener, + }) +} + +const hideAlert = state => { + return Object.assign({}, state, { + visible: false, + }) +} + +const map = { + [SHOW_ALERT]: showAlert, + [HIDE_ALERT]: hideAlert, +} + +export default reducerUtil(map, alertInitialState) diff --git a/src/modules/Alert/index.js b/src/modules/Alert/index.js new file mode 100644 index 0000000..abe0260 --- /dev/null +++ b/src/modules/Alert/index.js @@ -0,0 +1,3 @@ +import Alert from './Alert.container' + +export default Alert diff --git a/src/modules/App/Router.container.js b/src/modules/App/Router.container.js new file mode 100644 index 0000000..1ded0ac --- /dev/null +++ b/src/modules/App/Router.container.js @@ -0,0 +1,15 @@ +import { connect } from 'react-redux' +import { withRouter } from 'react-router-dom' +import Router from './Router' +import { fetchAllDappsAction } from '../Dapps/Dapps.reducer' + +const mapDispatchToProps = dispatch => ({ + fetchAllDapps: () => dispatch(fetchAllDappsAction()), +}) + +export default withRouter( + connect( + null, + mapDispatchToProps, + )(Router), +) diff --git a/src/modules/App/Router.jsx b/src/modules/App/Router.jsx new file mode 100644 index 0000000..8a13b60 --- /dev/null +++ b/src/modules/App/Router.jsx @@ -0,0 +1,45 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { Route, Switch } from 'react-router-dom' +import Home from '../Home' +import Filtered from '../Filtered' +import RecentlyAdded from '../RecentlyAdded' +import Profile from '../Profile' +import Dapps from '../Dapps' +import Vote from '../Vote' +import Submit from '../Submit' +import Terms from '../Terms/Terms' +import TransactionStatus from '../TransactionStatus' +import Alert from '../Alert' +import HowToSubmit from '../HowToSubmit' + +class Router extends React.Component { + componentDidMount() { + const { fetchAllDapps } = this.props + fetchAllDapps() + } + + render() { + return [ + + + + + + + + , + , + , + , + , + , + ] + } +} + +Router.propTypes = { + fetchAllDapps: PropTypes.func.isRequired, +} + +export default Router diff --git a/src/modules/App/index.js b/src/modules/App/index.js new file mode 100644 index 0000000..7ffa0ab --- /dev/null +++ b/src/modules/App/index.js @@ -0,0 +1,3 @@ +import Router from './Router.container' + +export default Router 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..e2512da --- /dev/null +++ b/src/modules/BlockchainExample/BlockchainExample.jsx @@ -0,0 +1,109 @@ +import React from 'react' +import exampleImage from './dapp.image' + +import BlockchainSDK from '../../common/blockchain' + +let SERVICES = '' + +const DAPP_DATA = { + name: 'Test1', + url: 'https://www.test1.com/', + description: 'Decentralized Test DApp', + category: 'test', + dateCreated: Date.now(), + image: exampleImage.image, +} + +// setTimeout is used in order to wait a transaction to be mined +const getResult = async function(method, params) { + return new Promise((resolve, reject) => { + setTimeout(async () => { + const result = await SERVICES.DiscoverService[method](...params) + resolve(result) + }, 2000) + }) +} + +/* + Each transaction-function return tx hash + createDApp returns tx hash + dapp id +*/ +class Example extends React.Component { + async getFullDApp(id) { + return getResult('getDAppDataById', [id]) + } + + async createDApp() { + await SERVICES.SNTService.generateTokens() + // return SERVICES.DiscoverService.createDApp(10000, DAPP_DATA) + } + + async upvote(id) { + return getResult('upVote', [id, 1000]) + } + + async downvote(id, amount) { + return getResult('downVote', [id, amount]) + } + + async withdraw(id) { + return getResult('withdraw', [id, 500]) + } + + async upVoteEffect(id) { + return getResult('upVoteEffect', [id, 10000]) + } + + async downVoteCost(id) { + return getResult('downVoteCost', [id]) + } + + async setMetadata(id) { + DAPP_DATA.category = 'updated' + return getResult('setMetadata', [id, DAPP_DATA]) + } + + async logDiscoverMethods() { + SERVICES = await BlockchainSDK.getInstance() + // console.log(await SERVICES.DiscoverService.getDApps()) + const createdDApp = await this.createDApp() + + // const dappData = await this.getFullDApp(createdDApp.id) + // console.log(`Created DApp : ${JSON.stringify(dappData)}`) + + // document.getElementById('testImage').src = dappData.metadata.image + + // const downVote = await this.downVoteCost(createdDApp.id) + // console.log( + // `Downvote TX Hash : ${await this.downvote(createdDApp.id, downVote.c)}`, + // ) + // console.log(`Upvote TX Hash : ${await this.upvote(createdDApp.id)}`) + // console.log(`Withdraw TX Hash : ${await this.withdraw(createdDApp.id)}`) + // console.log( + // `UpvoteEffect Result : ${await this.upVoteEffect(createdDApp.id)}`, + // ) + // console.log( + // `DownVoteCost Result : ${await this.downVoteCost(createdDApp.id)}`, + // ) + + // console.log( + // `Set metadata TX Hash : ${await this.setMetadata(createdDApp.id)}`, + // ) + // console.log( + // `Updated DApp : ${JSON.stringify( + // await this.getFullDApp(createdDApp.id), + // )}`, + // ) + } + + render() { + return ( +
+

+ +

+ ) + } +} + +export default Example 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/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 diff --git a/src/modules/Categories/Categories.container.js b/src/modules/Categories/Categories.container.js new file mode 100644 index 0000000..0b63631 --- /dev/null +++ b/src/modules/Categories/Categories.container.js @@ -0,0 +1,16 @@ +import { connect } from 'react-redux' +import { push } from 'connected-react-router' +// import { selectCategory } from '../CategorySelector/CategorySelector.reducer' +import Categories from './Categories' + +const mapDispatchToProps = dispatch => ({ + select: category => { + dispatch(push(`/categories/${category}`)) + // dispatch(selectCategory(category)) + }, +}) + +export default connect( + null, + mapDispatchToProps, +)(Categories) diff --git a/src/modules/Categories/Categories.jsx b/src/modules/Categories/Categories.jsx new file mode 100644 index 0000000..9f04b92 --- /dev/null +++ b/src/modules/Categories/Categories.jsx @@ -0,0 +1,47 @@ +import React from 'react' +import PropTypes from 'prop-types' +import categories from '../../common/utils/categories' +import styles from './Categories.module.scss' +import categoryImage from './Categories.utils' +import ViewAll from '../../common/components/ViewAll' + +const Categories = props => { + const { select } = props + const handleClick = category => select(category) + + return ( + <> +
+

Categories

+ +
+
+ {categories.map(category => ( + + ))} +
+ + ) +} + +Categories.propTypes = { + select: PropTypes.func.isRequired, +} + +export default Categories diff --git a/src/modules/Categories/Categories.module.scss b/src/modules/Categories/Categories.module.scss new file mode 100644 index 0000000..8599ebb --- /dev/null +++ b/src/modules/Categories/Categories.module.scss @@ -0,0 +1,81 @@ +@import '../../common/styles/variables'; + +.header { + display: flex; + justify-content: space-between; + margin: calculateRem(15); + align-items: center; +} + +.headline { + font-family: $font; + font-size: calculateRem(17); + margin: 0; +} + +.categories { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + grid-template-rows: 1fr 1fr 1fr; + margin: 0 calculateRem(10) calculateRem(30) calculateRem(10); + + @media (min-width: $desktop) { + grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr; + grid-template-rows: unset; + } +} + +.category { + background: $background; + font-family: $font; + font-size: calculateRem(13); + line-height: calculateRem(16); + color: $headline-color; + border: none; + border-radius: calculateRem(12); + padding: calculateRem(12); + margin: calculateRem(4); + cursor: pointer; + text-align: -webkit-center; + overflow: hidden; + display: flex; + flex-direction: column; + justify-content: flex-start; + + p { + margin-bottom: 0; + font-weight: 500; + } +} + +.EXCHANGES { + background: $purple-bg; +} + +.MARKETPLACES { + background: $orange-bg; +} + +.OTHER { + background: $yellow-bg; +} + +.MEDIA { + background: $yellow-bg; +} + +.GAMES { + background: $pink-bg; +} + +.COLLECTIBLES { + background: $blue-bg; +} + +.SOCIAL_NETWORKS { + background: $green-bg; +} + +.UTILITIES { + background: $red-bg; +} diff --git a/src/modules/Categories/Categories.utils.js b/src/modules/Categories/Categories.utils.js new file mode 100644 index 0000000..023b4cd --- /dev/null +++ b/src/modules/Categories/Categories.utils.js @@ -0,0 +1,20 @@ +import exchanges from '../../common/assets/images/categories/exchanges.svg' +import marketplaces from '../../common/assets/images/categories/marketplaces.svg' +import other from '../../common/assets/images/categories/other.svg' +import games from '../../common/assets/images/categories/games.svg' +import collectibles from '../../common/assets/images/categories/collectibles.svg' +import socialNetworks from '../../common/assets/images/categories/social-networks.svg' +import utilities from '../../common/assets/images/categories/utilities.svg' + +const imageMap = { + EXCHANGES: exchanges, + MARKETPLACES: marketplaces, + OTHER: other, + MEDIA: other, // TODO: fix with icon from design + GAMES: games, + COLLECTIBLES: collectibles, + SOCIAL_NETWORKS: socialNetworks, + UTILITIES: utilities, +} + +export default category => imageMap[category] diff --git a/src/modules/Categories/index.js b/src/modules/Categories/index.js new file mode 100644 index 0000000..7505d3b --- /dev/null +++ b/src/modules/Categories/index.js @@ -0,0 +1,3 @@ +import Categories from './Categories.container' + +export default Categories diff --git a/src/modules/CategoryHeader/CategoryHeader.jsx b/src/modules/CategoryHeader/CategoryHeader.jsx new file mode 100644 index 0000000..6d81722 --- /dev/null +++ b/src/modules/CategoryHeader/CategoryHeader.jsx @@ -0,0 +1,32 @@ +import React from 'react' +import PropTypes from 'prop-types' +import humanise from '../../common/utils/humanise' +import styles from './CategoryHeader.module.scss' +import CategoryIcon from '../../common/components/CategoryIcon' + +const CategoryHeader = props => { + const { text, active } = props + return ( +
+
+ +
+

{humanise(text)}

+
+ ) +} + +CategoryHeader.propTypes = { + text: PropTypes.string.isRequired, + active: PropTypes.bool, +} + +CategoryHeader.defaultProps = { + active: false, +} + +export default CategoryHeader diff --git a/src/modules/CategoryHeader/CategoryHeader.module.scss b/src/modules/CategoryHeader/CategoryHeader.module.scss new file mode 100644 index 0000000..a55baa6 --- /dev/null +++ b/src/modules/CategoryHeader/CategoryHeader.module.scss @@ -0,0 +1,30 @@ +@import '../../common/styles/variables'; + +.header { + background: $background; + font-family: 'Inter'; + display: flex; + align-items: center; + padding: calculateRem(15); + z-index: 99; + + &.active { + box-shadow: 0px -2px 8px rgba(0, 0, 0, 0.25); + position: fixed; + width: 100%; + top: 0; + } +} + +.icon { + margin-right: calculateRem(15); + + svg { + fill: $headline-color; + } +} + +.text { + font-size: calculateRem(15); + margin: 0; +} diff --git a/src/modules/CategoryHeader/index.js b/src/modules/CategoryHeader/index.js new file mode 100644 index 0000000..aea98ee --- /dev/null +++ b/src/modules/CategoryHeader/index.js @@ -0,0 +1,3 @@ +import CategoryHeader from './CategoryHeader' + +export default CategoryHeader diff --git a/src/modules/CategorySelector/CategorySelector.container.js b/src/modules/CategorySelector/CategorySelector.container.js new file mode 100644 index 0000000..71264f1 --- /dev/null +++ b/src/modules/CategorySelector/CategorySelector.container.js @@ -0,0 +1,21 @@ +import { connect } from 'react-redux' +import { push } from 'connected-react-router' +import CategorySelector from './CategorySelector' +import { closeDesktopAction } from '../DesktopMenu/DesktopMenu.reducer' +import { showHowToSubmitAction } from '../HowToSubmit/HowToSubmit.reducer' +// import { selectCategory } from './CategorySelector.reducer' + +// const mapStateToProps = state => ({ category: state.selectedCategory }) +const mapDispatchToProps = dispatch => ({ + select: category => { + dispatch(push(`/categories/${category}`)) + //dispatch(selectCategory(category)) + }, + onClickSubmit: () => dispatch(showHowToSubmitAction()), + onClickCloseDesktopMenu: () => dispatch(closeDesktopAction()), +}) + +export default connect( + null, + mapDispatchToProps, +)(CategorySelector) diff --git a/src/modules/CategorySelector/CategorySelector.jsx b/src/modules/CategorySelector/CategorySelector.jsx new file mode 100644 index 0000000..f3b503a --- /dev/null +++ b/src/modules/CategorySelector/CategorySelector.jsx @@ -0,0 +1,220 @@ +import React from 'react' +import PropTypes from 'prop-types' +import CategoryIcon from '../../common/components/CategoryIcon' +import ViewAll from '../../common/components/ViewAll' +import categories from '../../common/utils/categories' +import humanise from '../../common/utils/humanise' +import dropdownArrows from '../../common/assets/images/dropdown-arrows.svg' +import styles from './CategorySelector.module.scss' + +class CategorySelector extends React.Component { + constructor(props) { + super(props) + this.state = { open: false } + this.toggle = this.toggle.bind(this) + this.updateCategory = this.updateCategory.bind(this) + this.container = React.createRef() + this.onClickSubmit = this.onClickSubmit.bind(this) + this.onClickHighestRanked = this.onClickHighestRanked.bind(this) + this.onClickRecentlyAdded = this.onClickRecentlyAdded.bind(this) + } + + componentDidMount() { + this.closeOnBackgroundClick = this.closeOnBackgroundClick.bind(this) + document.addEventListener('click', this.closeOnBackgroundClick) + } + + componentWillUnmount() { + document.removeEventListener('click', this.closeOnBackgroundClick) + } + + onClickSubmit(e) { + const { onClickSubmit, onClickCloseDesktopMenu } = this.props + onClickCloseDesktopMenu() + onClickSubmit() + e.stopPropagation() + } + + closeOnBackgroundClick(event) { + if (this.container.current.contains(event.target)) { + return + } + + this.setState({ open: false }) + } + + onClickHighestRanked(e) { + const { onClickCloseDesktopMenu } = this.props + onClickCloseDesktopMenu() + e.stopPropagation() + window.location.hash = 'highest-ranked' + } + + onClickRecentlyAdded(e) { + const { onClickCloseDesktopMenu } = this.props + onClickCloseDesktopMenu() + e.stopPropagation() + window.location.hash = 'recently-added' + } + + updateCategory(event) { + const { select } = this.props + select(event.target.value) + this.setState({ open: false }) + } + + toggle() { + const { open } = this.state + this.setState({ open: !open }) + } + + render() { + const { + category, + alwaysOpen, + className, + showLists, + showSubmitDApp, + } = this.props + let { open } = this.state + if (alwaysOpen === true) open = true + + return ( +
+
+
+

Categories

+ +
+ {categories.map(c => ( + + ))} + + {showLists && ( + <> +
+

Lists

+
+ + + + )} + + {showSubmitDApp && ( + + )} +
+ + +
+ ) + } +} + +CategorySelector.propTypes = { + category: PropTypes.string, + select: PropTypes.func.isRequired, + alwaysOpen: PropTypes.bool, + className: PropTypes.string, + showLists: PropTypes.bool, + showSubmitDApp: PropTypes.bool, + onClickSubmit: PropTypes.func, + onClickCloseDesktopMenu: PropTypes.func, +} + +CategorySelector.defaultProps = { + category: null, + className: '', + alwaysOpen: false, + showLists: false, + showSubmitDApp: false, + onClickSubmit: null, + onClickCloseDesktopMenu: null, +} + +export default CategorySelector diff --git a/src/modules/CategorySelector/CategorySelector.module.scss b/src/modules/CategorySelector/CategorySelector.module.scss new file mode 100644 index 0000000..eba36e4 --- /dev/null +++ b/src/modules/CategorySelector/CategorySelector.module.scss @@ -0,0 +1,128 @@ +@import '../../common/styles/variables'; + +.open { + border-radius: 8px; + box-shadow: 0px 4px 12px rgba(0, 34, 51, 0.08), + 0px 2px 4px rgba(0, 34, 51, 0.16); + padding-top: calculateRem(12); + margin: calculateRem(12) calculateRem(16); + position: absolute; + background: $background; + width: calc(100% - 32px); + top: 0; + z-index: 1; + + h2 { + color: $text-color; + font-family: $font; + font-size: calculateRem(13); + margin: 0; + font-weight: normal; + } +} + +.openHeader { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0 calculateRem(16); + margin-bottom: calculateRem(12); +} + +.openHeader.spacing { + margin-top: 31px; +} + +.openButton { + display: block; + background: $background; + border: none; + font-family: $font; + font-size: calculateRem(15); + color: $headline-color; + display: flex; + width: 100%; + cursor: pointer; + align-content: center; + line-height: calculateRem(22); + padding: calculateRem(10) calculateRem(16); + + &:last-of-type { + border-bottom-left-radius: 8px; + border-bottom-right-radius: 8px; + } + + svg { + margin-right: calculateRem(19); + fill: $headline-color; + } +} + +.openButton.submitDapp { + margin-top: 24px; +} + +.selected { + background: $purple-bg; + color: $purple; + + svg { + fill: $purple; + } +} + +.closed { + display: flex; + width: 100%; + margin: calculateRem(12) calculateRem(16); + padding: calculateRem(10) calculateRem(15); + width: calc(100% - 32px); + box-shadow: 0px 2px 8px rgba(0, 0, 0, 0.1), + 0px 2px 6px rgba(136, 122, 249, 0.2); + border: none; + border-radius: 8px; + color: $background; + font-family: $font; + justify-content: space-between; + cursor: pointer; + align-items: center; +} + +.closedText { + display: flex; + align-items: center; + + svg { + fill: $background; + margin-right: calculateRem(19); + } +} + +.EXCHANGES { + background: $purple; +} + +.MARKETPLACES { + background: $orange; +} + +.COLLECTIBLES { + background: $blue; +} + +.GAMES { + background: $pink; +} + +.SOCIAL_NETWORKS { + background: $green; +} + +.UTILITIES { + background: $red; +} + +.OTHER { + background: $yellow; + color: $headline-color; +} diff --git a/src/modules/CategorySelector/CategorySelector.picker.js b/src/modules/CategorySelector/CategorySelector.picker.js new file mode 100644 index 0000000..caa5b01 --- /dev/null +++ b/src/modules/CategorySelector/CategorySelector.picker.js @@ -0,0 +1,12 @@ +import { connect } from 'react-redux' +import CategorySelector from './CategorySelector' +import { onSelectCategoryAction } from '../Submit/Submit.reducer' + +const mapDispatchToProps = dispatch => ({ + select: category => dispatch(onSelectCategoryAction(category)), +}) + +export default connect( + null, + mapDispatchToProps, +)(CategorySelector) diff --git a/src/modules/CategorySelector/CategorySelector.reducer.js b/src/modules/CategorySelector/CategorySelector.reducer.js new file mode 100644 index 0000000..0e9dc60 --- /dev/null +++ b/src/modules/CategorySelector/CategorySelector.reducer.js @@ -0,0 +1,19 @@ +// import reducerUtil from '../../common/utils/reducer' +// import { EXCHANGES } from '../../common/data/categories' + +// const UPDATE_CATEGORY = 'UPDATE_CATEGORY' + +// export const selectCategory = category => ({ +// type: UPDATE_CATEGORY, +// payload: category, +// }) + +// const initialState = EXCHANGES + +// const categoryChange = (_, category) => category + +// const map = { +// [UPDATE_CATEGORY]: categoryChange, +// } + +// export default reducerUtil(map, initialState) diff --git a/src/modules/CategorySelector/index.js b/src/modules/CategorySelector/index.js new file mode 100644 index 0000000..3c0b2ee --- /dev/null +++ b/src/modules/CategorySelector/index.js @@ -0,0 +1,3 @@ +import CategorySelector from './CategorySelector.container' + +export default CategorySelector diff --git a/src/modules/Dapps/Dapps.container.js b/src/modules/Dapps/Dapps.container.js new file mode 100644 index 0000000..152f04f --- /dev/null +++ b/src/modules/Dapps/Dapps.container.js @@ -0,0 +1,18 @@ +import { connect } from 'react-redux' +import Dapps from './Dapps' +// import selector from './Dapps.selector' +import { fetchByCategoryAction } from './Dapps.reducer' + +const mapStateToProps = state => ({ + dappsCategoryMap: state.dapps.dappsCategoryMap, +}) +const mapDispatchToProps = dispatch => ({ + fetchByCategory: category => { + dispatch(fetchByCategoryAction(category)) + }, +}) + +export default connect( + mapStateToProps, + mapDispatchToProps, +)(Dapps) diff --git a/src/modules/Dapps/Dapps.jsx b/src/modules/Dapps/Dapps.jsx new file mode 100644 index 0000000..879b0e0 --- /dev/null +++ b/src/modules/Dapps/Dapps.jsx @@ -0,0 +1,131 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { debounce } from 'debounce' +import DappList from '../../common/components/DappList' +import CategoryHeader from '../CategoryHeader' +import styles from './Dapps.module.scss' +import { headerElements, getYPosition } from './Dapps.utils' + +class Dapps extends React.Component { + static scanHeaderPositions() { + const headerPositions = headerElements().map(element => ({ + id: element.id, + position: getYPosition(element), + })) + return headerPositions + } + + constructor(props) { + super(props) + this.state = { + currentCategoryIndex: 0, + } + } + + componentDidMount() { + this.boundScroll = debounce(this.handleScroll.bind(this), 1) + window.addEventListener('scroll', this.boundScroll) + this.fetchDapps() + } + + componentDidUpdate() { + this.fetchDapps() + } + + componentWillUnmount() { + window.removeEventListener('scroll', this.boundScroll) + } + + onFetchByCategory(category) { + const { fetchByCategory } = this.props + fetchByCategory(category) + } + + getCategories() { + const { dappsCategoryMap } = this.props + return [...dappsCategoryMap.keys()] + } + + fetchDapps() { + const { dappsCategoryMap, fetchByCategory } = this.props + + dappsCategoryMap.forEach((dappState, category) => { + if (dappState.canFetch() === false) return + if (dappState.items.length >= 1) return + fetchByCategory(category) + }) + } + + handleScroll() { + const currentHeader = document.getElementById(this.currentCategory()) + const headerPositions = Dapps.scanHeaderPositions() + const categories = this.getCategories() + + const newHeader = [...headerPositions] + .reverse() + .find(header => header.position < window.scrollY) + + if (!newHeader) { + return this.setState({ currentCategoryIndex: 0 }) + } + + if (newHeader.id === currentHeader.id) { + return false + } + + const newIndex = categories.indexOf(newHeader.id) + + return this.setState({ currentCategoryIndex: newIndex }) + } + + currentCategory() { + const { currentCategoryIndex } = this.state + const categories = this.getCategories() + return categories[currentCategoryIndex] + } + + isCurrentCategory(category) { + return category === this.currentCategory() + } + + render() { + const { dappsCategoryMap } = this.props + const categories = this.getCategories() + + return ( +
+ {categories.map(category => ( +
+
+ +
+ + {dappsCategoryMap.get(category).canFetch() && ( +
+ Load more dApps from {category}{' '} +
+ )} +
+ ))} +
+ ) + } +} + +// Dapps.propTypes = { +// categories: PropTypes.arrayOf( +// PropTypes.shape({ category: PropTypes.string, dapps: DappListModel }), +// ).isRequired, +// } +Dapps.propTypes = { + dappsCategoryMap: PropTypes.instanceOf(Map).isRequired, + fetchByCategory: PropTypes.func.isRequired, +} + +export default Dapps diff --git a/src/modules/Dapps/Dapps.module.scss b/src/modules/Dapps/Dapps.module.scss new file mode 100644 index 0000000..f960085 --- /dev/null +++ b/src/modules/Dapps/Dapps.module.scss @@ -0,0 +1,16 @@ +@import '../../common/styles/variables'; + +.list { + margin-top: calculateRem(50); + margin-bottom: calculateRem(20); +} + +.loadMore { + color: $link-color; + text-transform: uppercase; + font-family: $font; + font-size: 12px; + font-weight: 600; + margin-left: calculateRem(40 + 16 + 16); + cursor: pointer; +} diff --git a/src/modules/Dapps/Dapps.reducer.js b/src/modules/Dapps/Dapps.reducer.js new file mode 100644 index 0000000..dba9995 --- /dev/null +++ b/src/modules/Dapps/Dapps.reducer.js @@ -0,0 +1,336 @@ +// import hardcodedDapps from '../../common/data/dapps' +import * as Categories from '../../common/data/categories' +import reducerUtil from '../../common/utils/reducer' +import { showAlertAction } from '../Alert/Alert.reducer' +import BlockchainSDK from '../../common/blockchain' +import { TYPE_SUBMIT } from '../TransactionStatus/TransactionStatus.utilities' + +const ON_FINISH_FETCH_ALL_DAPPS_ACTION = + 'DAPPS_ON_FINISH_FETCH_ALL_DAPPS_ACTION' + +const ON_START_FETCH_HIGHEST_RANKED = 'DAPPS_ON_START_FETCH_HIGHEST_RANKED' +const ON_FINISH_FETCH_HIGHEST_RANKED = 'DAPPS_ON_FINISH_FETCH_HIGHEST_RANKED' +const ON_START_FETCH_RECENTLY_ADDED = 'DAPPS_ON_START_FETCH_RECENTLY_ADDED' +const ON_FINISH_FETCH_RECENTLY_ADDED = 'DAPPS_ON_FINISH_FETCH_RECENTLY_ADDED' + +const ON_START_FETCH_BY_CATEGORY = 'DAPPS_ON_START_FETCH_BY_CATEGORY' +const ON_FINISH_FETCH_BY_CATEGORY = 'DAPPS_ON_FINISH_FETCH_BY_CATEGORY' + +const ON_UPDATE_DAPP_DATA = 'DAPPS_ON_UPDATE_DAPP_DATA' + +const RECENTLY_ADDED_SIZE = 50 +const HIGHEST_RANKED_SIZE = 50 + +class DappsState { + constructor() { + this.items = [] + this.hasMore = true + this.fetched = null + } + + canFetch() { + return this.hasMore && this.fetched !== true + } + + setFetched(fetched) { + this.fetched = fetched + } + + appendItems(items) { + const availableNames = new Set() + let addedItems = 0 + for (let i = 0; i < this.items.length; i += 1) + availableNames.add(this.items[i].name) + for (let i = 0; i < items.length; i += 1) { + if (availableNames.has(items[i].name) === false) { + addedItems += 1 + this.items.push(items[i]) + } + } + + this.hasMore = addedItems !== 0 + } + + cloneWeakItems() { + this.items = [...this.items] + return this + } +} + +export const onFinishFetchAllDappsAction = dapps => ({ + type: ON_FINISH_FETCH_ALL_DAPPS_ACTION, + payload: dapps, +}) + +export const onStartFetchHighestRankedAction = () => ({ + type: ON_START_FETCH_HIGHEST_RANKED, + payload: null, +}) + +export const onFinishFetchHighestRankedAction = highestRanked => ({ + type: ON_FINISH_FETCH_HIGHEST_RANKED, + payload: highestRanked, +}) + +export const onStartFetchRecentlyAddedAction = () => ({ + type: ON_START_FETCH_RECENTLY_ADDED, + payload: null, +}) + +export const onFinishFetchRecentlyAddedAction = recentlyAdded => ({ + type: ON_FINISH_FETCH_RECENTLY_ADDED, + payload: recentlyAdded, +}) + +export const onStartFetchByCategoryAction = category => ({ + type: ON_START_FETCH_BY_CATEGORY, + payload: category, +}) + +export const onFinishFetchByCategoryAction = (category, dapps) => ({ + type: ON_FINISH_FETCH_BY_CATEGORY, + payload: { category, dapps }, +}) + +const fetchAllDappsInState = async (dispatch, getState) => { + const state = getState() + const { transactionStatus } = state + const stateDapps = state.dapps + if (stateDapps.dapps === null) { + try { + const blockchain = await BlockchainSDK.getInstance() + let dapps = await blockchain.DiscoverService.getDApps() + dapps = dapps.map(dapp => { + return Object.assign(dapp.metadata, { + id: dapp.id, + sntValue: parseInt(dapp.effectiveBalance, 10), + }) + }) + dapps.sort((a, b) => { + return b.sntValue - a.sntValue + }) + if (transactionStatus.type === TYPE_SUBMIT) { + for (let i = 0; i < dapps.length; i += 1) { + if (dapps[i].id === transactionStatus.dappId) { + dapps.splice(i, 1) + break + } + } + } + + dispatch(onFinishFetchAllDappsAction(dapps)) + return dapps + } catch (e) { + dispatch(showAlertAction(e.message)) + dispatch(onFinishFetchAllDappsAction([])) + return [] + } + } + return stateDapps.dapps +} + +export const fetchAllDappsAction = () => { + return async (dispatch, getState) => { + dispatch(onStartFetchHighestRankedAction()) + dispatch(onStartFetchRecentlyAddedAction()) + + const dapps = await fetchAllDappsInState(dispatch, getState) + + const highestRanked = dapps.slice(0, HIGHEST_RANKED_SIZE) + let recentlyAdded = [...dapps] + recentlyAdded.sort((a, b) => { + return new Date().getTime(b.dateAdded) - new Date(a.dateAdded).getTime() + }) + recentlyAdded = recentlyAdded.slice(0, RECENTLY_ADDED_SIZE) + + dispatch(onFinishFetchHighestRankedAction(highestRanked)) + dispatch(onFinishFetchRecentlyAddedAction(recentlyAdded)) + } +} + +export const fetchByCategoryAction = category => { + return async (dispatch, getState) => { + dispatch(onStartFetchByCategoryAction(category)) + + const dapps = await fetchAllDappsInState(dispatch, getState) + const filteredByCategory = dapps.filter(dapp => dapp.category === category) + const dappsCategoryState = getState().dapps.dappsCategoryMap.get(category) + const from = dappsCategoryState.items.length + const to = Math.min(from + 5, filteredByCategory.length) + const dappsCategorySlice = filteredByCategory.slice(from, to) + + dispatch(onFinishFetchByCategoryAction(category, dappsCategorySlice)) + } +} + +export const onUpdateDappDataAction = dapp => ({ + type: ON_UPDATE_DAPP_DATA, + payload: dapp, +}) + +const onFinishFetchAllDapps = (state, dapps) => { + return Object.assign({}, state, { dapps }) +} + +const onStartFetchHightestRanked = state => { + return Object.assign({}, state, { + highestRankedFetched: false, + }) +} + +const onFinishFetchHighestRanked = (state, payload) => { + return Object.assign({}, state, { + highestRanked: payload, + highestRankedFetched: true, + }) +} + +const onStartFetchRecentlyAdded = state => { + return Object.assign({}, state, { + recentlyAddedFetched: false, + }) +} + +const onFinishFetchRecentlyAdded = (state, payload) => { + return Object.assign({}, state, { + recentlyAdded: payload, + recentlyAddedFetched: true, + }) +} + +const onStartFetchByCategory = (state, payload) => { + const dappsCategoryMap = new Map() + state.dappsCategoryMap.forEach((dappState, category) => { + dappsCategoryMap.set(category, dappState.cloneWeakItems()) + if (category === payload) dappState.setFetched(true) + }) + return Object.assign({}, state, { + dappsCategoryMap, + }) +} + +const onFinishFetchByCategory = (state, payload) => { + const { category, dapps } = payload + + const dappsCategoryMap = new Map() + state.dappsCategoryMap.forEach((dappState, category_) => { + dappsCategoryMap.set(category_, dappState) + if (category_ === category) { + dappState.setFetched(false) + dappState.appendItems(dapps) + } + }) + return Object.assign({}, state, { + dappsCategoryMap, + }) +} + +const insertDappIntoSortedArray = (source, dapp, cmp) => { + for (let i = 0; i < source.length; i += 1) { + if (cmp(source[i], dapp) === true) { + source.splice(i, 0, dapp) + break + } + } +} + +const onUpdateDappData = (state, dapp) => { + const dappsCategoryMap = new Map() + const { dapps } = state + let { highestRanked, recentlyAdded } = state + let update = false + + state.dappsCategoryMap.forEach((dappState, category_) => { + dappsCategoryMap.set(category_, dappState.cloneWeakItems()) + }) + + for (let i = 0; i < dapps.length; i += 1) { + if (dapps[i].id === dapp.id) { + dapps[i] = dapp + update = true + break + } + } + + if (update === false) { + insertDappIntoSortedArray(dapps, dapp, (target, dappItem) => { + return target.sntValue < dappItem.sntValue + }) + insertDappIntoSortedArray(highestRanked, dapp, (target, dappItem) => { + return target.sntValue < dappItem.sntValue + }) + highestRanked = state.highestRanked.splice(0, HIGHEST_RANKED_SIZE) + insertDappIntoSortedArray(recentlyAdded, dapp, (target, dappItem) => { + return ( + new Date().getTime(target.dateAdded) < + new Date(dappItem.dateAdded).getTime() + ) + }) + recentlyAdded = recentlyAdded.splice(0, RECENTLY_ADDED_SIZE) + + const dappState = dappsCategoryMap.get(dapp.category) + insertDappIntoSortedArray(dappState.items, dapp, (target, dappItem) => { + return target.sntValue < dappItem.sntValue + }) + } else { + for (let i = 0; i < highestRanked.length; i += 1) { + if (highestRanked[i].id === dapp.id) { + highestRanked[i] = dapp + break + } + } + for (let i = 0; i < recentlyAdded.length; i += 1) { + if (recentlyAdded[i].id === dapp.id) { + recentlyAdded[i] = dapp + break + } + } + dappsCategoryMap.forEach(dappState => { + const dappStateRef = dappState + for (let i = 0; i < dappStateRef.items.length; i += 1) { + if (dappStateRef.items[i].id === dapp.id) { + dappStateRef.items[i] = dapp + break + } + } + }) + } + + return Object.assign({}, state, { + dapps: [...dapps], + highestRanked: [...highestRanked], + recentlyAdded: [...recentlyAdded], + dappsCategoryMap, + }) +} + +const map = { + [ON_FINISH_FETCH_ALL_DAPPS_ACTION]: onFinishFetchAllDapps, + [ON_START_FETCH_HIGHEST_RANKED]: onStartFetchHightestRanked, + [ON_FINISH_FETCH_HIGHEST_RANKED]: onFinishFetchHighestRanked, + [ON_START_FETCH_RECENTLY_ADDED]: onStartFetchRecentlyAdded, + [ON_FINISH_FETCH_RECENTLY_ADDED]: onFinishFetchRecentlyAdded, + [ON_START_FETCH_BY_CATEGORY]: onStartFetchByCategory, + [ON_FINISH_FETCH_BY_CATEGORY]: onFinishFetchByCategory, + [ON_UPDATE_DAPP_DATA]: onUpdateDappData, +} + +const dappsCategoryMap = new Map() +dappsCategoryMap.set(Categories.EXCHANGES, new DappsState()) +dappsCategoryMap.set(Categories.MARKETPLACES, new DappsState()) +dappsCategoryMap.set(Categories.COLLECTIBLES, new DappsState()) +dappsCategoryMap.set(Categories.GAMES, new DappsState()) +dappsCategoryMap.set(Categories.SOCIAL_NETWORKS, new DappsState()) +dappsCategoryMap.set(Categories.UTILITIES, new DappsState()) +dappsCategoryMap.set(Categories.OTHER, new DappsState()) + +const dappsInitialState = { + dapps: null, + highestRanked: [], + highestRankedFetched: null, + recentlyAdded: [], + recentlyAddedFetched: null, + dappsCategoryMap, +} + +export default reducerUtil(map, dappsInitialState) diff --git a/src/modules/Dapps/Dapps.utils.js b/src/modules/Dapps/Dapps.utils.js new file mode 100644 index 0000000..b2c9d3f --- /dev/null +++ b/src/modules/Dapps/Dapps.utils.js @@ -0,0 +1,14 @@ +export const headerElements = () => + Array.from(document.querySelectorAll('.category-header')) + +export const getYPosition = element => { + let el = element + let yPosition = 0 + + while (el) { + yPosition += el.offsetTop - el.scrollTop + el.clientTop + el = el.offsetParent + } + + return yPosition +} diff --git a/src/modules/Dapps/index.js b/src/modules/Dapps/index.js new file mode 100644 index 0000000..76da169 --- /dev/null +++ b/src/modules/Dapps/index.js @@ -0,0 +1,3 @@ +import Dapps from './Dapps.container' + +export default Dapps diff --git a/src/modules/DesktopMenu/DesktopMenu.container.js b/src/modules/DesktopMenu/DesktopMenu.container.js new file mode 100644 index 0000000..fa9665e --- /dev/null +++ b/src/modules/DesktopMenu/DesktopMenu.container.js @@ -0,0 +1,14 @@ +import { connect } from 'react-redux' +import DesktopMenu from './DesktopMenu' +import { closeDesktopAction, showDesktopAction } from './DesktopMenu.reducer' + +const mapStateToProps = state => state.desktopMenu +const mapDispatchToProps = dispatch => ({ + onClickShow: () => dispatch(showDesktopAction()), + onClickClose: () => dispatch(closeDesktopAction()), +}) + +export default connect( + mapStateToProps, + mapDispatchToProps, +)(DesktopMenu) diff --git a/src/modules/DesktopMenu/DesktopMenu.jsx b/src/modules/DesktopMenu/DesktopMenu.jsx new file mode 100644 index 0000000..29f3808 --- /dev/null +++ b/src/modules/DesktopMenu/DesktopMenu.jsx @@ -0,0 +1,57 @@ +import React from 'react' +import PropTypes from 'prop-types' +import styles from './DesktopMenu.module.scss' +import CategorySelector from '../CategorySelector/CategorySelector.container' + +class DesktopMenu extends React.Component { + constructor(props) { + super(props) + this.nodes = { root: React.createRef() } + this.onClickBody = this.onClickBody.bind(this) + } + + componentDidMount() { + document.addEventListener('click', this.onClickBody) + } + + componentWillUnmount() { + document.removeEventListener('click', this.onClickBody) + } + + onClickBody(e) { + if (this.nodes.root.current.contains(e.target) === true) return + + const { onClickClose } = this.props + onClickClose() + } + + render() { + const { visible, onClickShow } = this.props + const cssClassVisible = visible ? styles.visible : '' + const cssClassNameVisibleDim = visible ? styles.dimVisible : '' + + return ( + <> +
+
+
+ +
+
+ + ) + } +} + +DesktopMenu.propTypes = { + visible: PropTypes.bool.isRequired, + onClickShow: PropTypes.func.isRequired, + onClickClose: PropTypes.func.isRequired, +} + +export default DesktopMenu diff --git a/src/modules/DesktopMenu/DesktopMenu.module.scss b/src/modules/DesktopMenu/DesktopMenu.module.scss new file mode 100644 index 0000000..45ea447 --- /dev/null +++ b/src/modules/DesktopMenu/DesktopMenu.module.scss @@ -0,0 +1,76 @@ +@import '../../common/styles/variables'; + +.dim { + position: fixed; + width: 100%; + height: 100%; + left: 0; + top: 0; + background: rgba(255, 255, 255, 0.5); + z-index: 20; + + transition-duration: 0.4s; + transition-property: opacity; +} + +.dim:not(.dimVisible) { + opacity: 0; + pointer-events: none; +} + +.cnt { + width: 40px; + height: 40px; + display: none; + align-items: center; + justify-content: center; + position: absolute; + left: 20px; + top: 60px; + border-radius: 50%; + background: #fff; + z-index: 1; + box-shadow: 0px 4px 12px rgba(0, 34, 51, 0.08), + 0px 2px 4px rgba(0, 34, 51, 0.16); + transform: translateY(-50%); + cursor: pointer; + z-index: 24; + + @media (min-width: $desktop) { + display: flex; + } +} + +.cnt:before { + content: ''; + width: 16px; + height: 6px; + border-top: 2px solid #000; + border-bottom: 2px solid #000; +} + +.dropDown { + width: 320px; + position: absolute; + left: 0; + top: 0; + opacity: 0; + transition-property: opacity; + transition-duration: 0.4s; +} + +.dropDown.visible { + opacity: 1; +} + +.dropDown:not(.visible) { + pointer-events: none; +} + +.dropDown .categorySelector > * { + margin: 0; +} + +.dropDown .categorySelector * { + user-select: none; +} diff --git a/src/modules/DesktopMenu/DesktopMenu.reducer.js b/src/modules/DesktopMenu/DesktopMenu.reducer.js new file mode 100644 index 0000000..d59b6d9 --- /dev/null +++ b/src/modules/DesktopMenu/DesktopMenu.reducer.js @@ -0,0 +1,34 @@ +import desktopMenuState from '../../common/data/desktop-menu' +import reducerUtil from '../../common/utils/reducer' + +const SHOW_DESKTOP_MENU = 'SHOW_DESKTOP_MENU' +const CLOSE_DESKTOP_MENU = 'CLOSE_DESKTOP_MENU' + +export const showDesktopAction = () => ({ + type: SHOW_DESKTOP_MENU, + payload: null, +}) + +export const closeDesktopAction = () => ({ + type: CLOSE_DESKTOP_MENU, + payload: null, +}) + +const showDesktopMenu = state => { + return Object.assign({}, state, { + visible: true, + }) +} + +const hideDesktopMenu = state => { + return Object.assign({}, state, { + visible: false, + }) +} + +const map = { + [SHOW_DESKTOP_MENU]: showDesktopMenu, + [CLOSE_DESKTOP_MENU]: hideDesktopMenu, +} + +export default reducerUtil(map, desktopMenuState) diff --git a/src/modules/Filtered/Filtered.container.js b/src/modules/Filtered/Filtered.container.js new file mode 100644 index 0000000..69a84e5 --- /dev/null +++ b/src/modules/Filtered/Filtered.container.js @@ -0,0 +1,17 @@ +import { connect } from 'react-redux' +import Filtered from './Filtered' +import { fetchByCategoryAction } from '../Dapps/Dapps.reducer' + +const mapStateToProps = state => ({ + dappsCategoryMap: state.dapps.dappsCategoryMap, +}) +const mapDispatchToProps = dispatch => ({ + fetchByCategory: category => { + dispatch(fetchByCategoryAction(category)) + }, +}) + +export default connect( + mapStateToProps, + mapDispatchToProps, +)(Filtered) diff --git a/src/modules/Filtered/Filtered.jsx b/src/modules/Filtered/Filtered.jsx new file mode 100644 index 0000000..f5c3e11 --- /dev/null +++ b/src/modules/Filtered/Filtered.jsx @@ -0,0 +1,89 @@ +import React from 'react' +import PropTypes from 'prop-types' +import CategorySelector from '../CategorySelector' +import DappList from '../../common/components/DappList' +import styles from './Filtered.module.scss' + +const getScrollY = + window.scrollY !== undefined + ? () => { + return window.scrollY + } + : () => { + return document.documentElement.scrollTop + } + +class Filtered extends React.Component { + constructor(props) { + super(props) + this.onScroll = this.onScroll.bind(this) + } + + componentDidMount() { + this.fetchDapps() + document.addEventListener('scroll', this.onScroll) + } + + componentDidUpdate() { + this.fetchDapps() + } + + onScroll() { + this.fetchDapps() + } + + getDappList() { + const { dappsCategoryMap, match } = this.props + const result = + match !== undefined ? dappsCategoryMap.get(match.params.id).items : [] + return result + } + + fetchDapps() { + const { dappsCategoryMap, match, fetchByCategory } = this.props + if (match === undefined) return + + const dappState = dappsCategoryMap.get(match.params.id) + if (dappState.canFetch() === false) return + + const root = document.getElementById('root') + const bottom = window.innerHeight + getScrollY() + const isNearEnd = bottom + window.innerHeight > root.offsetHeight + + if (isNearEnd === false && dappState.items.length >= 10) return + + fetchByCategory(match.params.id) + } + + render() { + const { match } = this.props + const result = this.getDappList() + + return ( + <> + +
+ +
+ + ) + } +} + +Filtered.defaultProps = { + match: undefined, +} + +Filtered.propTypes = { + dappsCategoryMap: PropTypes.instanceOf(Map).isRequired, + fetchByCategory: PropTypes.func.isRequired, + match: PropTypes.shape({ + params: PropTypes.shape({ + id: PropTypes.node, + }).isRequired, + }), +} + +export default Filtered diff --git a/src/modules/Filtered/Filtered.module.scss b/src/modules/Filtered/Filtered.module.scss new file mode 100644 index 0000000..d907b76 --- /dev/null +++ b/src/modules/Filtered/Filtered.module.scss @@ -0,0 +1,5 @@ +@import '../../common/styles/variables'; + +.list { + margin-bottom: calculateRem(20); +} diff --git a/src/modules/Filtered/Filtered.selector.js b/src/modules/Filtered/Filtered.selector.js new file mode 100644 index 0000000..6471ee5 --- /dev/null +++ b/src/modules/Filtered/Filtered.selector.js @@ -0,0 +1,10 @@ +// import { createSelector } from 'reselect' + +// const getCategory = state => state.selectedCategory +// const getDapps = state => state.dapps + +// export default createSelector( +// [getCategory, getDapps], +// (category, dapps) => +// category ? dapps.filter(dapp => dapp.category === category) : dapps, +// ) diff --git a/src/modules/Filtered/Filtered.selector.test.js b/src/modules/Filtered/Filtered.selector.test.js new file mode 100644 index 0000000..f7a0cab --- /dev/null +++ b/src/modules/Filtered/Filtered.selector.test.js @@ -0,0 +1,38 @@ +import filteredDapps from './Filtered.selector' + +describe('filteredDapps', () => { + const dapps = [ + { + name: 'DAPP_1', + category: 'CATEGORY_1', + }, + { + name: 'DAPP_2', + category: 'CATEGORY_2', + }, + ] + + test('it should return all the dapps when the category is not set', () => { + // Given a state where the selected category is null + const state = { + dapps, + selectedCategory: null, + } + + // We expect to get back all the dapps + expect(filteredDapps(state)).toEqual(dapps) + }) + + test('it should return only the matching dapps when the category is set', () => { + // Given a state where the selected category is set + const state = { + dapps, + selectedCategory: 'CATEGORY_1', + } + + // We expect to get back only the matching dapps + expect(filteredDapps(state)).toEqual([ + { name: 'DAPP_1', category: 'CATEGORY_1' }, + ]) + }) +}) diff --git a/src/modules/Filtered/index.js b/src/modules/Filtered/index.js new file mode 100644 index 0000000..05197fe --- /dev/null +++ b/src/modules/Filtered/index.js @@ -0,0 +1,3 @@ +import Filtered from './Filtered.container' + +export default Filtered diff --git a/src/modules/Footer/Footer.container.js b/src/modules/Footer/Footer.container.js new file mode 100644 index 0000000..b1658ec --- /dev/null +++ b/src/modules/Footer/Footer.container.js @@ -0,0 +1,12 @@ +import { connect } from 'react-redux' +import Footer from './Footer' +import { showHowToSubmitAction } from '../HowToSubmit/HowToSubmit.reducer' + +const mapDispatchToProps = dispatch => ({ + onClickSubmit: () => dispatch(showHowToSubmitAction()), +}) + +export default connect( + null, + mapDispatchToProps, +)(Footer) diff --git a/src/modules/Footer/Footer.jsx b/src/modules/Footer/Footer.jsx new file mode 100644 index 0000000..c0d36eb --- /dev/null +++ b/src/modules/Footer/Footer.jsx @@ -0,0 +1,60 @@ +import React from 'react' +import PropTypes from 'prop-types' +import styles from './Footer.module.scss' +import communityIcon from '../../common/assets/images/community.svg' +import addDappIcon from '../../common/assets/images/add-dapp.svg' +import supportIcon from '../../common/assets/images/support.svg' + +const Footer = props => { + const { onClickSubmit } = props + + return ( + + ) +} + +Footer.propTypes = { + onClickSubmit: PropTypes.func.isRequired, +} + +export default Footer diff --git a/src/modules/Footer/Footer.module.scss b/src/modules/Footer/Footer.module.scss new file mode 100644 index 0000000..d6e05ef --- /dev/null +++ b/src/modules/Footer/Footer.module.scss @@ -0,0 +1,63 @@ +@import '../../common//styles/variables'; + +.footer { + display: flex; + flex-direction: column; + background-color: #eef2f5; + font-family: $font; + padding: calculateRem(40) calculateRem(16) calculateRem(32) calculateRem(16); + margin-top: calculateRem(24); +} + +.footerItem { + text-decoration: none; + display: flex; + align-items: flex-start; + margin-bottom: calculateRem(24); + cursor: pointer; + + :last-of-type { + margin-bottom: 0; + } + + h2 { + color: $headline-color; + font-size: calculateRem(15); + line-height: calculateRem(22); + margin-bottom: calculateRem(2); + margin-top: calculateRem(12); + font-weight: 500; + } + + p { + color: $text-color; + font-size: calculateRem(13); + line-height: calculateRem(18); + margin-bottom: calculateRem(2); + margin-top: 0; + } +} + +.iconWrap { + background: $text-color; + padding: calculateRem(10); + padding-bottom: calculateRem(6); + border-radius: 50%; + margin-top: calculateRem(15); + margin-right: calculateRem(16); +} + +@media (min-width: $desktop) { + .footer { + flex-direction: row; + } + + .footerItem:not(:first-child) { + margin-left: 34px; + } + + .footerItem { + width: 0; + flex: 1 1 auto; + } +} diff --git a/src/modules/Footer/index.js b/src/modules/Footer/index.js new file mode 100644 index 0000000..cf89615 --- /dev/null +++ b/src/modules/Footer/index.js @@ -0,0 +1,3 @@ +import Footer from './Footer.container' + +export default Footer diff --git a/src/modules/HighestRanked/HighestRanked.container.js b/src/modules/HighestRanked/HighestRanked.container.js new file mode 100644 index 0000000..2a70388 --- /dev/null +++ b/src/modules/HighestRanked/HighestRanked.container.js @@ -0,0 +1,8 @@ +import { connect } from 'react-redux' +import HighestRanked from './HighestRanked' + +const mapStateToProps = state => ({ + dapps: state.dapps.highestRanked, +}) + +export default connect(mapStateToProps)(HighestRanked) diff --git a/src/modules/HighestRanked/HighestRanked.jsx b/src/modules/HighestRanked/HighestRanked.jsx new file mode 100644 index 0000000..e6b8f7b --- /dev/null +++ b/src/modules/HighestRanked/HighestRanked.jsx @@ -0,0 +1,25 @@ +import React from 'react' +import { DappListModel } from '../../common/utils/models' +import DappList from '../../common/components/DappList' +import styles from './HighestRanked.module.scss' + +const HighestRanked = props => { + const { dapps } = props + + return ( + <> +

+ Highest Ranked +

+
+ +
+ + ) +} + +HighestRanked.propTypes = { + dapps: DappListModel.isRequired, +} + +export default HighestRanked diff --git a/src/modules/HighestRanked/HighestRanked.module.scss b/src/modules/HighestRanked/HighestRanked.module.scss new file mode 100644 index 0000000..b97272a --- /dev/null +++ b/src/modules/HighestRanked/HighestRanked.module.scss @@ -0,0 +1,32 @@ +@import '../../common/styles/variables'; + +.headline { + font-family: $font; + font-size: calculateRem(17); + margin-left: calculateRem(15); + margin-bottom: calculateRem(10); +} + +.grid { + display: grid; + grid-auto-flow: column; + grid-auto-columns: calc(90%); + // grid-template-rows: 1fr 1fr 1fr; + overflow-x: scroll; + overflow-y: hidden; + -webkit-overflow-scrolling: touch; + + @media (min-width: $desktop) { + grid-auto-flow: row; + grid-template-columns: 1fr 1fr; + overflow-x: hidden; + } + + @media (min-width: 970px) { + grid-template-columns: 1fr 1fr 1fr; + } + + @media (min-width: 1300px) { + grid-template-columns: 1fr 1fr 1fr 1fr; + } +} diff --git a/src/modules/HighestRanked/index.js b/src/modules/HighestRanked/index.js new file mode 100644 index 0000000..1dad046 --- /dev/null +++ b/src/modules/HighestRanked/index.js @@ -0,0 +1,3 @@ +import HighestRanked from './HighestRanked.container' + +export default HighestRanked diff --git a/src/modules/Home/Home.container.js b/src/modules/Home/Home.container.js new file mode 100644 index 0000000..ec93b99 --- /dev/null +++ b/src/modules/Home/Home.container.js @@ -0,0 +1,6 @@ +import { connect } from 'react-redux' +import Home from './Home' + +const mapStateToProps = state => state + +export default connect(mapStateToProps)(Home) diff --git a/src/modules/Home/Home.jsx b/src/modules/Home/Home.jsx new file mode 100644 index 0000000..e108eb0 --- /dev/null +++ b/src/modules/Home/Home.jsx @@ -0,0 +1,60 @@ +import React from 'react' +import PropTypes from 'prop-types' +import RecentlyAdded from '../RecentlyAdded' +import HighestRanked from '../HighestRanked' +import Categories from '../Categories' +import FeaturedDapps from '../../common/components/FeatureDapps' +import Footer from '../Footer' +import LoadingHome from '../LoadingHome' +import featured from '../../common/data/featured' +import styles from './Home.module.scss' +import DesktopMenu from '../DesktopMenu/DesktopMenu.container' + +class Home extends React.Component { + constructor(props) { + super(props) + this.state = {} + } + + render() { + const { dapps } = this.props + const loaded = + dapps.highestRankedFetched === true && dapps.recentlyAddedFetched === true + + return ( + <> + {loaded && ( + <> +
+

Discover

+
+ + + + + +