Browse Source

Initial cleanup

develop
Ezerous 4 years ago
parent
commit
cb67aab662
  1. 8
      .babelrc
  2. 15
      .eslintrc.js
  3. 15
      .gitignore
  4. 17
      .npmignore
  5. 5
      .travis.yml
  6. 13
      CHANGELOG.md
  7. 120
      CONTRIBUTING.md
  8. 2
      LICENSE
  9. 379
      README.md
  10. 42
      index.d.ts
  11. 13
      jest.config.js
  12. 66
      package.json
  13. BIN
      readme/drizzle-logomark.png
  14. BIN
      readme/drizzle-sync1.png
  15. BIN
      readme/drizzle-sync2.png
  16. BIN
      readme/drizzle-sync3.png
  17. BIN
      readme/drizzle-sync4.png
  18. 1
      src/DrizzleContract.js
  19. 2
      src/accountBalances/accountBalancesSaga.js
  20. 2
      src/accounts/accountsSaga.js
  21. 8
      src/blocks/blocksSaga.js
  22. 3
      src/defaultOptions.js
  23. 2
      src/drizzleStatus/drizzleStatusSaga.js
  24. 4
      src/mergeOptions.js
  25. 4
      src/web3/web3Saga.js
  26. 53
      test/accountBalances.test.js
  27. 38
      test/accounts.test.js
  28. 91
      test/blocks.test.js
  29. 122
      test/contractStateUtils.test.js
  30. 167
      test/drizzle/api.test.js
  31. 48
      test/drizzle/getOrCreateWeb3.test.js
  32. 184
      test/drizzle/middleware.test.js
  33. 75
      test/drizzle/options.test.js
  34. 45
      test/environments/ganache-environment.js
  35. 67
      test/generateStore.test.js
  36. 42
      test/utils/data/TestContract-abi.json
  37. 6
      test/utils/data/TestContract-byteCode.json
  38. 12
      test/utils/data/TestContract.sol
  39. 60
      test/utils/helpers.js
  40. 176
      test/web3.test.js
  41. 36
      types/Drizzle.d.ts
  42. 83
      types/IContract.d.ts
  43. 25
      types/contractStateUtils.d.ts
  44. 13
      types/generateStore.d.ts
  45. 3
      types/index.d.ts
  46. 30
      webpack/base.config.js
  47. 6
      webpack/pure.config.js
  48. 14
      webpack/release.config.js
  49. 2744
      yarn.lock

8
.babelrc

@ -1,8 +0,0 @@
{
"presets": ["@babel/preset-env"],
"plugins": [
"@babel/plugin-transform-runtime",
"@babel/plugin-transform-arrow-functions",
"@babel/plugin-proposal-object-rest-spread"
]
}

15
.eslintrc.js

@ -1,15 +0,0 @@
module.exports = {
"extends": ["standard", "plugin:jest/recommended"],
"plugins": ["jest"],
"rules": {
"jest/prefer-to-have-length": "warn",
"space-before-function-paren": ["error", {
"anonymous": "ignore",
"asyncArrow": "always",
"named": "ignore"
}]
},
"env" : {
"jest/globals": true
}
}

15
.gitignore

@ -1,5 +1,12 @@
# Node
/node_modules
# IDE
.DS_Store
.tern-project
dist
node_modules
yarn*
.idea
# Logs
/log
npm-debug.log*
yarn-debug.log*
yarn-error.log*

17
.npmignore

@ -1,10 +1,9 @@
.DS_Store
jest.config.js
.travis.yml
.eslintrc.js
.babelrc
# Jetbrains
.idea
node_modules
test
webpack
.github
# Git
.gitattributes
# Package managers
yarn.lock
*.tgz

5
.travis.yml

@ -1,5 +0,0 @@
language: node_js
node_js:
- "11.10.1"
- "--lts"
cache: npm

13
CHANGELOG.md

@ -1,13 +0,0 @@
# Change Log
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## 1.5.1 (2019-10-29)
### Bug Fixes
* handle customProvider option correctly ([be2488f](https://github.com/trufflesuite/drizzle/commit/be2488f))
* readme.md ([d0da8b5](https://github.com/trufflesuite/drizzle/commit/d0da8b5))
* readme.md ([ac187f5](https://github.com/trufflesuite/drizzle/commit/ac187f5))

120
CONTRIBUTING.md

@ -1,120 +0,0 @@
Contributing to Drizzle
=======================
_Thanks for taking the time to help out and improve Drizzle! :tada:_
The following is a set of guidelines for Drizzle contributions and may change over time. Feel free to suggest improvements to this document in a pull request!
Contents
--------
[How Can I Contribute?](#how-can-i-contribute)
[Development](#development)
- [Overview](#overview)
- [Development Requirements](#development-requirements)
- [Getting Started](#getting-started)
- [Forks, Branches, and Pull Requests](#forks-branches-and-pull-requests)
- [Branching Model](#branching-model)
- [Working on a Branch](#working-on-a-branch)
[Additional Notes](#additional-notes)
How Can I Contribute?
---------------------
All contributions are welcome!
If you run into an issue, the first step is to reach out in [our community Gitter channel](https://gitter.im/ConsenSys/truffle), in case others have run into the problem or know how to help.
To report a problem or to suggest a new feature, [open a GitHub Issue](https://github.com/trufflesuite/drizzle/issues/new). This will help the Drizzle maintainers become aware of the problem and prioritize a fix.
For code contributions, for either new features or bug fixes, see [Development](#development).
If you're looking to make a substantial change, you may want to reach out first to give us a heads up.
Development
-----------
### Overview
Drizzle has two companion libraries ([`drizzle-react`](https://github.com/trufflesuite/drizzle-react) and [`drizzle-react-components`](https://github.com/trufflesuite/drizzle-react-components)), each with their own NPM packages.
The content of this guide applies to those companion libraries as well.
This repository ([trufflesuite/drizzle](https://github.com/trufflesuite/drizzle)) contains the core logic for storing and updating chaindata in a [Redux](https://github.com/reduxjs/redux) store.
### Development Requirements
In order to develop Drizzle, you'll need:
- [Git](https://git-scm.com/)
- [Node.js](https://nodejs.org)
### Getting Started
First clone this repository and install NPM dependencies:
$ git clone git@github.com:trufflesuite/drizzle.git
$ cd drizzle
$ npm install
$ npm test
If all is good, then run the build command :
$ npm run build
Your local Drizzle copy is contained in the `dist/` directory.
To use this in a project, use `npm link`:
$ cd dist
$ npm link // may require sudo
$ cd my-project-root
$ npm link drizzle
You're ready to use your local development version of Drizzle in your project.
### Forks, Branches, and Pull Requests
Community contributions to Drizzle require that you first fork each repository you wish to modify. After your modifications, push changes to your fork(s) and submit a pull request upstream to `trufflesuite`'s fork(s).
See GitHub documentation about [Collaborating with issues and pull requests](https://help.github.com/categories/collaborating-with-issues-and-pull-requests/) for more information.
> :exclamation: **Note:** _Drizzle development uses a long-lived `develop` branch for new (non-hotfix) development. Pull Requests should be opened against `develop` in all repositories._
#### Branching Model
Drizzle projects adhere to Gitflow, a Git workflow designed around a strict branching model to more easily track feature development vs. releases. [For more information on Gitflow, check out Atlassian's helpful guide](https://www.atlassian.com/git/tutorials/comparing-workflows/gitflow-workflow).
We can separate our branches into long-lived and purposeful branches. We have two long-lived branches:
- **`master`**, checkout for hotfix development; in sync with the latest `release` (synced after the release has gone out publicly).
- **`develop`**, checkout for feature development; latest unstable releases and work targeting the next major or minor release.
All development is done on branches with a `prefix/title` style naming convention. These are later merged into `develop` and finally a `release` branch before final release. These are the two prefixes we use:
- **`feature/`**, for new feature development; later merged with `develop` and `release`.
- **`fix/`**, for hotfix development; later merged with `master` and `develop`.
For example, a fix for a contract fetching error might look like: `fix/contract-fetching`.
#### Working on a Branch
Use a branch for your modifications, tracking it on your fork:
$ git checkout -b feature/sweet-feature // or "fix/" prefix if a hotfix
$ git push -u ChocolateLover feature/sweet-feature
Then, make changes and commit as usual.
Additional Notes
----------------
Join the chat in [our community Gitter channel](https://gitter.im/ConsenSys/truffle). If anything about this process is unclear, or for helpful feedback of any kind, we'd love to hear from you!
**Thanks again for all your support, encouragement, and effort! Drizzle would not be possible without contributors like you. :bow:**

2
LICENSE.txt → LICENSE

@ -1,5 +1,7 @@
MIT License
Copyright (c) 2020 Ezerous
Copyright (c) 2017 Joshua Quintal
Permission is hereby granted, free of charge, to any person obtaining a copy

379
README.md

@ -1,378 +1,3 @@
<img src="https://truffleframework.com/img/drizzle-logo-dark.svg" width="200">
# @ezerous/drizzle
# @drizzle/store
`npm install --save @drizzle/store`
Drizzle is a collection of front-end libraries that make writing dapp frontends easier and more predictable. The core of Drizzle is based on a Redux store, so you have access to the spectacular development tools around Redux. We take care of synchronizing your contract data, transaction data and more. Things stay fast because you declare what to keep in sync.
- Fully reactive contract data, including state, events and transactions.
- Declarative, so you're not wasting valuable cycles on uneeded data.
- Maintains access to underlying functionality. Web3 and your contract's methods are still there, untouched.
**Using React?**: The easiest way to get started with Drizzle is to use our [official `@drizzle/react-plugin` package](https://github.com/trufflesuite/drizzle/tree/master/packages/react-plugin) and (optionally) its companion [`@drizzle/react-components`](https://github.com/trufflesuite/drizzle/tree/master/packages/react-components).
## Getting Started
**Note**: Since Drizzle uses web3 1.0 and web sockets, be sure your development environment can support these.
1. Import the provider.
```javascript
import { Drizzle } from "@drizzle/store";
```
1. Create an `options` object and pass in the desired contract artifacts for Drizzle to instantiate. Other options are available, see [the Options section](#options) below.
```javascript
// Import contracts
import SimpleStorage from "./../build/contracts/SimpleStorage.json";
import TutorialToken from "./../build/contracts/TutorialToken.json";
const options = {
contracts: [SimpleStorage]
};
const drizzle = new Drizzle(options);
```
**Note**: The above assumes you have no existing redux store and will generate a new one. If you need something more sophisticated, consult our documentation [Using Drizzle's Redux Store](https://www.truffleframework.com/docs/drizzle/getting-started/using-drizzles-redux-store)
1. Get contract data. Calling the `cacheCall()` function on a contract will
execute the desired call and return a corresponding key so the data can be
retrieved from the store. When a new block is received, Drizzle will refresh
the store automatically _if_ any transactions in the block touched our
contract. For more information on how this works, see [How Data Stays
Fresh](#how-data-stays-fresh).
**Note:** We have to check that Drizzle is initialized before fetching data. A simple if statement such as below is fine for displaying a few pieces of data, but a better approach for larger dapps is to use a [loading component](https://github.com/trufflesuite/drizzle-react#recipe-loading-component). We've already built one for you in our [`@drizzle/react-components` library](https://github.com/trufflesuite/drizzle/tree/master/packages/react-components) as well.
```javascript
// Assuming we're observing the store for changes.
const state = drizzle.store.getState();
// If Drizzle is initialized (and therefore web3, accounts and contracts), continue.
if (state.drizzleStatus.initialized) {
// Declare this call to be cached and synchronized. We'll receive the store key for recall.
const dataKey = drizzle.contracts.SimpleStorage.methods.storedData.cacheCall();
// Use the dataKey to display data from the store.
return state.contracts.SimpleStorage.storedData[dataKey].value;
}
// If Drizzle isn't initialized, display some loading indication.
return "Loading...";
```
The contract instance has all of its standard web3 properties and methods. For example, you could still call as normal if you don't want something in the store:
```javascript
drizzle.contracts.SimpleStorage.methods.storedData().call();
```
1. Send a contract transaction. Calling the `cacheSend()` function on a contract will send the desired transaction and return a corresponding transaction hash so the status can be retrieved from the store. The last argument can optionally be an options object with the typical from, gas and gasPrice keys. Drizzle will update the transaction's state in the store (pending, success, error) and store the transaction receipt. For more information on how this works, see [How Data Stays Fresh](#how-data-stays-fresh).
**Note:** We have to check that Drizzle is initialized before fetching data. A simple if statement such as below is fine for displaying a few pieces of data, but a better approach for larger dapps is to use a [loading component](https://github.com/trufflesuite/drizzle/tree/master/packages/react-plugin#recipe-loading-component). We've already built one for you in our [`@drizzle/react-components` library](https://github.com/trufflesuite/drizzle/tree/master/packages/react-components) as well.
```javascript
// Assuming we're observing the store for changes.
const state = drizzle.store.getState();
// If Drizzle is initialized (and therefore web3, accounts and contracts), continue.
if (state.drizzleStatus.initialized) {
// Declare this transaction to be observed. We'll receive the stackId for reference.
const stackId = drizzle.contracts.SimpleStorage.methods.set.cacheSend(2, {
from: "0x3f..."
});
// Use the dataKey to display the transaction status.
if (state.transactionStack[stackId]) {
const txHash = state.transactionStack[stackId];
return state.transactions[txHash].status;
}
}
// If Drizzle isn't initialized, display some loading indication.
return "Loading...";
```
For more information on what's contained in transaction state, see [Drizzle State](#drizzle-state).
The contract instance has all of its standard web3 properties and methods. For example, you could still send as normal if you don't want a tx in the store:
```javascript
drizzle.contracts.SimpleStorage.methods.set(2).send({ from: "0x3f..." });
```
## Adding contracts dynamically
You can programmatically add contracts to Drizzle using either `drizzle.addContract()` or the `ADD_CONTRACT` action.
```javascript
const contractConfig = {
contractName: "0x066408929e8d5Ed161e9cAA1876b60e1fBB5DB75",
web3Contract: new web3.eth.Contract(/* ... */)
};
events = ["Mint"];
// Using an action
dispatch({ type: "ADD_CONTRACT", drizzle, contractConfig, events, web3 });
// Or using the Drizzle context object
this.context.drizzle.addContract(contractConfig, events);
```
## Deleting contracts
You can also delete contracts using either `drizzle.deleteContract()` or the `DELETE_CONTRACT` action.
```javascript
const contractName = "MyContract";
// Using an action
dispatch({ type: "DELETE_CONTRACT", drizzle, contractName });
// Or using the Drizzle context object
this.context.drizzle.deleteContract(contractName);
```
## Options
Drizzle has a number of configuration options so it only keeps track of exactly the data you need. Here's the full list of options along with their default values.
```javascript
{
contracts,
events: {
contractName: [
eventName,
{
eventName,
eventOptions
}
]
},
polls: {
accounts: interval,
blocks: interval
},
syncAlways,
web3: {
customProvider,
fallback: {
type
url
}
},
networkWhitelist
}
```
### `contracts` (array)
An array of either contract artifact files or Web3 contract objects. The objects have a `contractName` and `web3Contract` key.
i.e.
```
contracts: [
truffleArtifact, // A regular Truffle contract artifact
{
contractName: 'RegisteredContract',
web3Contract: new web3.eth.Contract(abi, address, {data: 'deployedBytecode' }) // An instance of a Web3 contract
}
]
```
### `events` (object)
An object consisting of contract names each containing an array of strings of the event names we'd like to listen for and sync with the store. Furthermore, event names may be replaced with an object containing both `eventName` and `eventOptions`, where `eventOptions` field corresponds to the [web3 Contract.events options](https://web3js.readthedocs.io/en/v1.2.0/web3-eth-contract.html#contract-events).
### `polls` (object)
An object containing key/value pairs denoting what is being polled and the interval (in ms). Possible polls are accounts and blocks. Accounts will poll for addresses and balances, blocks for new blocks. **Default**: `{ blocks: 3000 }`
### `syncAlways` (boolean)
If `true`, will replay all contract calls at every block. This is useful if your dapp uses a proxy contract which obfuscates your primary contract's address. By default Drizzle checks blocks to see if a transaction interacting with your contracts has occured. If so, it syncs that contract. **Default**: `false`
### `web3` (object)
Options regarding `web3` instantiation.
#### `customProvider` (object)
A valid web3 `provider` object. For example, you may wish to programatically create a Ganache provider for testing:
```
// Create a Ganache provider.
const testingProvider = Ganache.provider({
gasLimit: 7000000
})
const options = {
web3: {
customProvider: testingProvider
}
}
const drizzle = new Drizzle(options)
```
#### `fallback` (object)
An object consisting of the type and url of a fallback web3 provider. This is used if no injected provider, such as MetaMask or Mist, is detected.
`type` (string): The type of the fallback web3 provider. Currently the only possibility is `'ws'` (web socket). **Default**: `'ws'`
`url` (string): The full fallback web3 provider url. **Default**: `'ws://127.0.0.1:8545'`
### `networkWhitelist` (array)
An array of valid network ids for your project. Your smart contracts might only be deployed on particular networks, or you might want to restrict access on networks that are under development.
Allows all networks by default. Ganache bypasses this check and is never restricted.
```
// Allows the listed networks, plus Ganache
const options = {
networkWhitelist: [
1, // Mainnet
3, // Ropsten
4, // Rinkeby
5, // Goerli
42 // Kovan
]
}
```
## Drizzle State
```javascript
{
accounts,
accountBalances: {
address
}
contracts: {
contractName: {
initialized,
synced,
events,
callerFunctionName: {
argsHash: {
args,
value
}
}
}
},
currentBlock,
drizzleStatus: {
initialized
},
transactions: {
txHash: {
confirmations,
error,
receipt,
status
}
},
transactionStack,
web3: {
status
}
}
```
## `accounts` (array)
An array of account addresses from `web3`.
## `accountBalances` (object)
An object whose keys are account addresses and values are account balances (in Wei).
## `contracts` (object)
A series of contract state objects, indexed by the contract name as declared in its ABI.
### `contractName` (object)
`initialized` (boolean): `true` once contract is fully instantiated.
`synced` (boolean): `false` if contract state changes have occurred in a block and Drizzle is re-running its calls.
`events` (array): An array of event objects. Drizzle will only listen for the events we declared in options.
The contract's state also includes the state of each constant function called on the contract (`callerFunctionName`). The functions are indexed by name, and contain the outputs indexed by a hash of the arguments passed during the call (`argsHash`). If no arguments were passed, the hash is `0x0`. Drizzle reads from the store for you, so it should be unnecessary to touch this data cache manually.
`args` (array): Arguments passed to function call.
`value` (mixed): Value returned from function call.
### `currentBlock` (object)
An object the latest block as an object resulting from [`web3.getBlock()`](https://web3js.readthedocs.io/en/v1.2.0/web3-eth.html#getblock). This is updated once the block is received from a subscription or fetched via polling, but before any processing takes place.
## `drizzleStatus` (object)
An object containing information about the status of Drizzle.
`initialized` (boolean): `true` once:
- `web3` is found or instantiated
- Account addresses are stored in state
- All contracts are instantiated
### `initialized` (boolean)
`false` by default, becomes true once a `web3` instance is found and the accounts and contracts are fetched.
## `transactions` (object)
A series of transaction objects, indexed by transaction hash.
### `txHash` (object)
`confirmations` (array): After the initial receipt, further confirmation receipts (up to the 24th).
`error` (object): contains the returned error if any.
`receipt` (object): contains the first transaction receipt received from a transaction's `success` event.
`status` (string): `true` or `false` depending on transaction status
- `pending` when the transaction has broadcasted successfully, but is not yet mined
- `success` when a transaction receipt has been received (you may also wish to check for further confirmations)
- `error` if any errors occurred after broadcasting
For more in-depth information on the Ethereum transaction lifecycle, [check out this great blog post](https://medium.com/blockchannel/life-cycle-of-an-ethereum-transaction-e5c66bae0f6e).
## `transactionStack` (array)
In cases where a user cancels a transaction or the transaction is malformed and unable to be broadcasted, it won't receive a hash. To keep track of these cases, a temporary ID will be added to this array and replaced with the transaction hash once broadcasted. The `cacheSend()` method will return a `stackId`, which will allow you get the temporary ID to observe this process for your own transaction status indicator UI.
## `web3` (object)
`status` (string): `initializing`, `initialized` and `failed` are possible options. Useful for triggering warnings if `web3` fails to instantiate.
## How Data Stays Fresh
1. Once initialized, Drizzle instantiates `web3` and our desired contracts, then observes the chain by subscribing to new block headers.
![Drizzle Sync Step 1](https://github.com/trufflesuite/drizzle/blob/master/packages/store/readme/drizzle-sync1.png?raw=true)
1. Drizzle keeps track of contract calls so it knows what to synchronize.
![Drizzle Sync Step 2](https://github.com/trufflesuite/drizzle/blob/master/packages/store/readme/drizzle-sync2.png?raw=true)
1. When a new block header comes in, Drizzle checks that the block isn't pending, then goes through the transactions looking to see if any of them touched our contracts.
![Drizzle Sync Step 3](https://github.com/trufflesuite/drizzle/blob/master/packages/store/readme/drizzle-sync3.png?raw=true)
1. If they did, we replay the calls already in the store to refresh any potentially altered data. If they didn't we continue with the store data.
![Drizzle Sync Step 4](https://github.com/trufflesuite/drizzle/blob/master/packages/store/readme/drizzle-sync4.png?raw=true)
## License
[MIT](https://github.com/trufflesuite/drizzle/blob/master/packages/store/LICENSE.txt)
A reactive data-store for web3 and smart contracts. A modified version of [@drizzle/store](https://github.com/trufflesuite/drizzle/tree/develop/packages/store).

42
index.d.ts

@ -1,42 +0,0 @@
import {
Drizzle,
IDrizzleOptions,
generateStore,
IStoreConfig,
generateContractsInitialState,
} from './types';
type drizzleSagas = any[];
export {
Drizzle,
IDrizzleOptions,
generateStore,
IStoreConfig,
generateContractsInitialState,
drizzleSagas,
};
export enum EventActions {
EVENT_FIRED = 'EVENT_FIRED',
EVENT_CHANGED = 'EVENT_CHANGED',
EVENT_ERROR = 'EVENT_ERROR',
}
export namespace drizzleReducers {
type state = any | undefined | null;
export interface IAction {
[key: string]: any;
type: string;
}
export function accounts(state: state, action: IAction): any;
export function accountBalances(state: state, action: IAction): any;
export function contracts(state: state, action: IAction): any;
export function currentBlock(state: state, action: IAction): any;
export function drizzleStatus(state: state, action: IAction): any;
export function transactions(state: state, action: IAction): any;
export function transactionStack(state: state, action: IAction): any;
export function web3(state: state, action: IAction): any;
}

13
jest.config.js

@ -1,13 +0,0 @@
// For a detailed explanation regarding each configuration property, visit:
// https://jestjs.io/docs/en/configuration.html
module.exports = {
// Automatically clear mock calls and instances between every test
clearMocks: true,
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
modulePathIgnorePatterns: ['<rootDir>[/\\\\](dist|node_modules)[/\\\\]'],
// The test environment that will be used for testing
testEnvironment: '<rootDir>/test/environments/ganache-environment.js'
}

66
package.json

@ -1,59 +1,17 @@
{
"name": "@drizzle/store",
"version": "1.5.1",
"name": "@ezerous/drizzle",
"version": "0.1.0",
"description": "A reactive data-store for web3 and smart contracts.",
"types": "./index.d.ts",
"main": "./dist/drizzle-store.js",
"react-native": "./src/index.js",
"repository": "https://github.com/trufflesuite/drizzle",
"scripts": {
"prepare": "webpack --config webpack/release.config.js",
"build:pure": "webpack --config webpack/pure.config.js",
"dev": "webpack --config webpack/release.config.js --watch",
"format": "prettier-standard 'src/**/*.js' 'test/**/*.js'",
"lint": "eslint 'src/**/*.js'",
"lint:fix": "eslint 'src/**/*.js' --fix",
"test": "jest --notify --silent",
"webpack-report": "webpack-bundle-analyzer --log-level debug --port 4200 dist/stats.json"
},
"keywords": [
"ethereum",
"redux",
"redux-saga"
],
"author": {
"name": "Josh Quintal",
"email": "josh@trufflesuite.com",
"url": "http://truffleframework.com/docs/drizzle/getting-started"
},
"license": "ISC",
"publishConfig": {
"access": "public"
},
"license": "MIT",
"author": "Ezerous <ezerous@gmail.com>",
"main": "src/index.js",
"repository": "github:Ezerous/drizzle",
"dependencies": {
"deepmerge": "^3.2.0",
"is-plain-object": "^2.0.4",
"redux": "^4.0.1",
"redux-saga": "^0.16.0",
"web3": "^1.2.1"
},
"devDependencies": {
"@babel/core": "^7.4.4",
"@babel/preset-env": "^7.4.4",
"eslint-config-standard": "^13.0.1",
"eslint-plugin-import": "^2.18.2",
"eslint-plugin-jest": "^22.14.1",
"eslint-plugin-node": "^9.1.0",
"eslint-plugin-promise": "^4.2.1",
"eslint-plugin-standard": "^4.0.0",
"eth-block-tracker-es5": "^2.3.2",
"ganache-core": "^2.5.5",
"jest": "^24.7.1",
"prettier-standard": "^9.1.1",
"redux-mock-store": "^1.5.3",
"standard": "^13.1.0",
"webpack": "^4.30.0",
"webpack-cli": "^3.3.1",
"webpack-merge": "^4.2.1"
"deepmerge": "4.2.2",
"eth-block-tracker": "4.4.3",
"is-plain-object": "4.1.1",
"redux": "4.0.5",
"redux-saga": "1.1.3",
"web3": "1.2.6"
}
}

BIN
readme/drizzle-logomark.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.9 KiB

BIN
readme/drizzle-sync1.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

BIN
readme/drizzle-sync2.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

BIN
readme/drizzle-sync3.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

BIN
readme/drizzle-sync4.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

1
src/DrizzleContract.js

@ -1,6 +1,7 @@
import * as ContractActions from './contracts/constants'
import * as TransactionsActions from './transactions/constants'
import { isGetterFunction, isSetterFunction } from './contractStateUtils'
class DrizzleContract {
constructor (
web3Contract,

2
src/accountBalances/accountBalancesSaga.js

@ -10,7 +10,7 @@ export function * getAccountBalances (action) {
}
try {
for (var i in accounts) {
for (let i in accounts) {
var account = accounts[i]
var accountBalance = yield call(web3.eth.getBalance, account)

2
src/accounts/accountsSaga.js

@ -1,4 +1,4 @@
import { END, eventChannel } from 'redux-saga'
import { eventChannel } from 'redux-saga'
import { call, put, take, takeLatest } from 'redux-saga/effects'
import { getAccountBalances } from '../accountBalances/accountBalancesSaga'
import * as AccountsActions from './constants'

8
src/blocks/blocksSaga.js

@ -1,6 +1,6 @@
import { END, eventChannel } from 'redux-saga'
import { call, put, take, takeEvery, takeLatest, all } from 'redux-saga/effects'
import BlockTracker from 'eth-block-tracker-es5'
import PollingBlockTracker from 'eth-block-tracker'
import * as BlocksActions from './constants'
import * as ContractActions from '../contracts/constants'
@ -65,7 +65,7 @@ export function createBlockPollChannel ({
syncAlways
}) {
return eventChannel(emit => {
const blockTracker = new BlockTracker({
const blockTracker = new PollingBlockTracker({
provider: web3.currentProvider,
pollingInterval: interval
})
@ -81,8 +81,8 @@ export function createBlockPollChannel ({
const unsubscribe = () => {
blockTracker.stop().catch(_ => {
// BlockTracker assumes there is an outstanding event subscription.
// However for our tests we start and stop a BlockTracker in succession
// PollingBlockTracker assumes there is an outstanding event subscription.
// However for our tests we start and stop a PollingBlockTracker in succession
// that triggers an error.
})
}

3
src/defaultOptions.js

@ -1,8 +1,5 @@
const defaultOptions = {
web3: {
// `block` no longer needed;
// keeping for pre-v1.1.1 compatibility with drizzle-react.
block: false,
fallback: {
type: 'ws',
url: 'ws://127.0.0.1:8545'

2
src/drizzleStatus/drizzleStatusSaga.js

@ -1,4 +1,4 @@
import { call, put, select, takeLatest } from 'redux-saga/effects'
import { call, put, takeLatest } from 'redux-saga/effects'
// Initialization Functions
import { initializeWeb3, getNetworkId } from '../web3/web3Saga'

4
src/mergeOptions.js

@ -1,5 +1,5 @@
import merge from 'deepmerge'
const isPlainObject = require('is-plain-object')
const merge = require('deepmerge');
import isPlainObject from 'is-plain-object';
export default function (defaultOptions, newOptions) {
return merge(defaultOptions, newOptions, {

4
src/web3/web3Saga.js

@ -1,7 +1,7 @@
import { call, put } from 'redux-saga/effects'
import * as Action from './constants'
var Web3 = require('web3')
const Web3 = require('web3');
/*
* Initialization
@ -47,7 +47,7 @@ export function * initializeWeb3 (options) {
// Attempt fallback if no web3 injection.
switch (options.fallback.type) {
case 'ws':
var provider = new Web3.providers.WebsocketProvider(
const provider = new Web3.providers.WebsocketProvider(
options.fallback.url
)
web3 = new Web3(provider)

53
test/accountBalances.test.js

@ -1,53 +0,0 @@
import * as AccountBalancesActions from '../src/accountBalances/constants'
import {
getAccountBalances,
getAccountsState
} from '../src/accountBalances/accountBalancesSaga'
import { call, put, select } from 'redux-saga/effects'
describe('Account Balance Saga', () => {
let mockedWeb3, mockedGetBalance
let gen
beforeEach(() => {
mockedGetBalance = jest.fn()
mockedWeb3 = { eth: { getBalance: mockedGetBalance } }
gen = getAccountBalances({ web3: mockedWeb3 })
})
test('Retrieves account balances', () => {
let next = gen.next()
expect(next.value).toEqual(select(getAccountsState))
next = gen.next(global.accounts)
// It handles balance queries for all accounts
const accountBalance = 1e20 // default ETH balance
for (let account of global.accounts) {
expect(next.value).toEqual(call(mockedGetBalance, account))
next = gen.next(accountBalance)
expect(next.value).toEqual(
put({ type: AccountBalancesActions.ACCOUNT_BALANCE_FETCHED, account, accountBalance })
)
next = gen.next()
}
// Final dispatch
expect(next.value).toEqual(put({ type: AccountBalancesActions.ACCOUNT_BALANCES_FETCHED }))
})
test('Fails properly', () => {
let next = gen.next()
expect(next.value).toEqual(select(getAccountsState))
next = gen.next(global.accounts)
const error = new Error()
next = gen.throw(error)
expect(next.value).toEqual(put({ type: AccountBalancesActions.ACCOUNT_BALANCE_FAILED, error }))
// Final dispatch
next = gen.next()
expect(next.value).toEqual(put({ type: AccountBalancesActions.ACCOUNT_BALANCES_FETCHED }))
})
})

38
test/accounts.test.js

@ -1,38 +0,0 @@
import { getAccounts } from '../src/accounts/accountsSaga'
import { call, put } from 'redux-saga/effects'
import * as AccountsActions from '../src/accounts/constants'
describe('Accounts Saga', () => {
let mockedWeb3, mockedGetAccounts
let gen
beforeEach(() => {
mockedGetAccounts = jest.fn()
mockedWeb3 = { eth: { getAccounts: mockedGetAccounts } }
gen = getAccounts({ web3: mockedWeb3 })
})
test('retrieves Metamask accounts', () => {
expect(gen.next().value).toEqual(call(mockedGetAccounts))
expect(gen.next(global.accounts).value).toEqual(
put({ type: AccountsActions.ACCOUNTS_FETCHED, accounts: global.accounts })
)
})
describe('Fails', () => {
test('when accounts are not retrieved', () => {
expect(gen.next().value).toEqual(call(mockedGetAccounts))
expect(gen.next(undefined).value).toEqual(
put({ type: AccountsActions.ACCOUNTS_FAILED, error: 'No accounts found!' })
)
})
test('when when an exception occurs', () => {
const error = new Error()
expect(gen.next().value).toEqual(call(mockedGetAccounts))
expect(gen.throw(error).value).toEqual(
put({ type: AccountsActions.ACCOUNTS_FAILED, error })
)
})
})
})

91
test/blocks.test.js

@ -1,91 +0,0 @@
import {
createBlockChannel,
createBlockPollChannel
} from '../src/blocks/blocksSaga'
import { getWeb3 } from './utils/helpers'
import * as BlocksActions from '../src/blocks/constants'
describe('read from blocks', () => {
let web3
let syncAlways
const drizzle = {}
beforeAll(() => {
web3 = getWeb3()
syncAlways = false
})
describe('by listening through websockets', () => {
let blockListener
beforeEach(() => {
blockListener = createBlockChannel({ drizzle, web3, syncAlways })
})
test('listens for block headers', done => {
// Subscribe to event
blockListener.take(event => {
expect(event.type).toEqual(BlocksActions.BLOCK_RECEIVED)
done()
})
// Invoke action to trigger event
web3.eth.sendTransaction({
from: global.accounts[0],
to: global.accounts[1],
value: 200
})
})
test('unsubscribes from block headers', done => {
// Subscribe to event
blockListener.take(event => {
expect(event.type).toEqual('@@redux-saga/CHANNEL_END')
done()
})
// Invoke action to trigger event
blockListener.close()
})
})
describe('by polling', () => {
let blockPoller
beforeEach(() => {
const interval = 1000
blockPoller = createBlockPollChannel({
drizzle,
interval,
web3,
syncAlways
})
})
test('polls for block headers', done => {
// Subscribe to event
blockPoller.take(event => {
expect(event.type).toEqual(BlocksActions.BLOCK_FOUND)
done()
})
// Invoke action to trigger event
web3.eth.sendTransaction({
from: global.accounts[0],
to: global.accounts[1],
value: 200
})
})
test('terminates from block polling', done => {
// Subscribe to event
blockPoller.take(event => {
expect(event.type).toEqual('@@redux-saga/CHANNEL_END')
done()
})
// Invoke action to trigger event
blockPoller.close()
})
})
})

122
test/contractStateUtils.test.js

@ -1,122 +0,0 @@
import {
isGetterFunction,
isSetterFunction,
getAbi,
generateContractInitialState,
generateContractsInitialState
} from '../src/contractStateUtils'
import TestContractABI from './utils/data/TestContract-abi.json'
describe('Contract State Utilities', () => {
describe('isConstant', () => {
test('can identify a constant for solc v0.5.16 and below', () => {
const config = { type: 'function', constant: true }
expect(isGetterFunction(config)).toBe(true)
expect(isSetterFunction(config)).toBe(false)
})
test('can identify non constants for solc v0.5.16 and below', () => {
let config = { type: 'function', constant: false }
expect(isGetterFunction(config)).toBe(false)
expect(isSetterFunction(config)).toBe(true)
config = { type: 'event' }
expect(isGetterFunction(config)).toBe(false)
expect(isSetterFunction(config)).toBe(false)
})
test('can identify a constant for a pure or view func, for breaking changes from solc v0.6.0 and above', () => {
let config = { type: 'function', stateMutability: 'pure' }
expect(isGetterFunction(config)).toBe(true)
expect(isSetterFunction(config)).toBe(false)
config = { type: 'function', stateMutability: 'view' }
expect(isGetterFunction(config)).toBe(true)
expect(isSetterFunction(config)).toBe(false)
})
test('can identify non constants, for breaking changes from solc v0.6.0 and above', () => {
let config = { type: 'function', stateMutability: 'payable' }
expect(isGetterFunction(config)).toBe(false)
expect(isSetterFunction(config)).toBe(true)
config = { type: 'function', stateMutability: 'nonpayable' }
expect(isGetterFunction(config)).toBe(false)
expect(isSetterFunction(config)).toBe(true)
config = { type: 'event' }
expect(isGetterFunction(config)).toBe(false)
expect(isSetterFunction(config)).toBe(false)
})
})
describe('getAbi', () => {
test('can parse Web3 contract', () => {
const jsonInterface = {}
const web3Contract = { options: { jsonInterface } }
expect(getAbi({ web3Contract })).toEqual(jsonInterface)
})
test('can parse TruffleArtifact', () => {
const abi = {}
const artifact = { abi }
expect(getAbi(artifact)).toEqual(abi)
})
})
describe('generateContractinitialState', () => {
test('It generates correct state from truffleArtifact', () => {
const expectedState = {
initialized: false,
synced: false,
storedData: {}
}
const input = { abi: TestContractABI }
expect(generateContractInitialState(input)).toEqual(expectedState)
})
test('It generates correct state from Web3 Contract', () => {
const expectedState = {
initialized: false,
synced: false,
storedData: {}
}
const input = {
web3Contract: { options: { jsonInterface: TestContractABI } }
}
expect(generateContractInitialState(input)).toEqual(expectedState)
})
})
describe('generateContractsInitialState', () => {
test('it generates multi-contract initial state', () => {
const contracts = [
{ contractName: 'C1', abi: TestContractABI },
{ contractName: 'C2', abi: TestContractABI }
]
const expectedStates = {
C1: {
initialized: false,
synced: false,
storedData: {}
},
C2: {
initialized: false,
synced: false,
storedData: {}
}
}
expect(generateContractsInitialState({ contracts })).toEqual(
expectedStates
)
})
test('it generates valid initial state with empty contracts', () => {
expect(generateContractsInitialState({})).toEqual({})
})
})
})

167
test/drizzle/api.test.js

@ -1,167 +0,0 @@
import { put } from 'redux-saga/effects'
import MockedDrizzleContract from '../../src/DrizzleContract'
import { getWeb3Assets } from '../utils/helpers'
import Drizzle from '../../src/Drizzle'
import defaultDrizzleOptions from '../../src/defaultOptions'
import { initializeDrizzle } from '../../src/drizzleStatus/drizzleStatusSaga'
import { NETWORK_IDS, NETWORK_MISMATCH } from '../../src/web3/constants'
import * as DrizzleActions from '../../src/drizzleStatus/constants'
import * as ContractActions from '../../src/contracts/constants'
jest.mock('../../src/DrizzleContract')
describe('Drizzle API', () => {
const accounts = global.accounts
const contractName = 'TestContract'
let dispatchSpy, mockedStore, state, networkId
const drizzleOptions = {}
let drizzle
let contractCreatorSpy
beforeEach(() => {
MockedDrizzleContract.mockClear()
networkId = global.defaultNetworkId
// Mock Store
state = { web3: { networkId }, accounts }
dispatchSpy = jest.fn()
mockedStore = { dispatch: dispatchSpy, getState: () => state }
// Create Drizzle and simulate web3 resolution
contractCreatorSpy = jest.fn()
let mockedWeb3 = { eth: { Contract: contractCreatorSpy } }
drizzle = new Drizzle(drizzleOptions, mockedStore)
drizzle.web3 = mockedWeb3
// Only the contractName is required for these tests
MockedDrizzleContract.mockImplementation(() => ({ contractName }))
})
test('Constructor fires up drizzle store', () => {
const expectedAction = {
type: DrizzleActions.DRIZZLE_INITIALIZING,
drizzle,
options: defaultDrizzleOptions
}
expect(dispatchSpy).toHaveBeenCalledWith(expectedAction)
})
// Default values in drizzleOptions
describe('Default drizzle options', () => {
// networkWhiteList
test('Empty network whitelist does not trigger a mismatch', () => {
networkId = NETWORK_IDS.ropsten
// Iterate to 3rd effect in initializeDrizzle generator
let gen = initializeDrizzle({drizzle, options: drizzleOptions})
let next = gen.next() // initializeWeb3
const fakeWeb3 = {eth: {}};
next = gen.next(fakeWeb3) // getNetworkId
// Replace saga networkId with our own
next = gen.next(networkId) // networkWhitelist check
const unExpectedAction = put({ type: NETWORK_MISMATCH, networkId })
expect(next.value).not.toEqual(unExpectedAction)
})
})
describe('Add:', () => {
test('a Web3 Contracts', () => {
const web3Contract = {}
const contractConfig = { web3Contract, contractName }
drizzle.addContract(contractConfig)
// 1 in constructor, 2 in addContract
expect(dispatchSpy).toHaveBeenCalledTimes(3)
let expectedAction = { type: ContractActions.CONTRACT_INITIALIZING, contractConfig }
expect(dispatchSpy).toHaveBeenNthCalledWith(2, expectedAction)
expectedAction = { type: ContractActions.CONTRACT_INITIALIZED, name: contractName }
expect(dispatchSpy).toHaveBeenNthCalledWith(3, expectedAction)
expect(drizzle.contractList).toHaveLength(1)
expect(drizzle.contracts).toHaveProperty(contractName)
expect(MockedDrizzleContract).toHaveBeenCalledTimes(1)
})
test('a TruffleArtifact Contracts', async () => {
const { truffleArtifact } = await getWeb3Assets()
drizzle.addContract(truffleArtifact)
// 1 in constructor, 2 in addContract
expect(dispatchSpy).toHaveBeenCalledTimes(3)
let expectedAction = {
type: ContractActions.CONTRACT_INITIALIZING,
contractConfig: truffleArtifact
}
expect(dispatchSpy).toHaveBeenNthCalledWith(2, expectedAction)
expectedAction = {
type: ContractActions.CONTRACT_INITIALIZED,
name: truffleArtifact.contractName
}
expect(dispatchSpy).toHaveBeenNthCalledWith(3, expectedAction)
expect(drizzle.contractList).toHaveLength(1)
expect(drizzle.contracts).toHaveProperty(truffleArtifact.contractName)
expect(MockedDrizzleContract).toHaveBeenCalledTimes(1)
})
test('does not add duplicate contract', () => {
const web3Contract = {}
const contractConfig = { web3Contract, contractName }
MockedDrizzleContract.mockImplementation(() => ({ contractName }))
drizzle.addContract(contractConfig)
// Only called on 1st add
expect(dispatchSpy).toHaveBeenCalledTimes(3)
expect(drizzle.contractList).toHaveLength(1)
expect(MockedDrizzleContract).toHaveBeenCalledTimes(1)
// Try to add the same contract
const chucker = () => drizzle.addContract(contractConfig)
expect(chucker).toThrow(/^Contract already exists: TestContract/)
// No more calls to dispatch
expect(dispatchSpy).toHaveBeenCalledTimes(3)
})
})
describe('Delete:', () => {
test('removes a contract', () => {
// Add a contract
const web3Contract = {}
const contractConfig = { web3Contract, contractName }
drizzle.addContract(contractConfig)
expect(drizzle.contractList).toHaveLength(1)
drizzle.deleteContract(contractName)
expect(drizzle.contractList).toHaveLength(0)
// 3 calls to add, 1 to delete
expect(dispatchSpy).toHaveBeenCalledTimes(4)
})
test('throws if contract does not exist', () => {
expect(drizzle.contractList).toHaveLength(0)
const eraser = () => drizzle.deleteContract('Transmogrify')
expect(eraser).toThrow(/^Contract does not exist: Transmogrify/)
// 1 call in ctor to initialize drizzle
expect(dispatchSpy).toHaveBeenCalledTimes(1)
})
})
})

48
test/drizzle/getOrCreateWeb3.test.js

@ -1,48 +0,0 @@
import { getOrCreateWeb3Contract } from '../../src/Drizzle'
describe('getOrCreateWeb3Contract', () => {
const networkId = global.defaultNetworkId
const accounts = global.accounts
let mockedStore, state
beforeEach(() => {
state = { web3: { networkId }, accounts }
mockedStore = { getState: () => state }
})
test('recognizes a web3 contract', () => {
const mockedWeb3Contract = {}
const mockedContractConfig = { web3Contract: mockedWeb3Contract }
const resolved = getOrCreateWeb3Contract(
mockedStore,
mockedContractConfig,
{}
)
expect(resolved).toBe(mockedWeb3Contract)
})
test('recognizes a truffleArtifact', () => {
const address = '0x0123456789'
const abi = 'ABI'
const deployedBytecode = "I am Jack's caffeine fueled ledger code"
const mockedTruffleArtifact = {
abi,
networks: { [networkId]: { address } },
deployedBytecode
}
const contractCreatorSpy = jest.fn()
const mockedWeb3 = { eth: { Contract: contractCreatorSpy } }
getOrCreateWeb3Contract(mockedStore, mockedTruffleArtifact, mockedWeb3)
// Default selected is the 1st by convention
const selectedAccount = accounts[0]
const expectedArgs = [
abi,
address,
{ from: selectedAccount, data: deployedBytecode }
]
expect(contractCreatorSpy).toHaveBeenCalledWith(...expectedArgs)
})
})

184
test/drizzle/middleware.test.js

@ -1,184 +0,0 @@
import MockedDrizzleContract from '../../src/DrizzleContract'
import { drizzleMiddleware } from '../../src/drizzle-middleware'
import Drizzle from '../../src/Drizzle'
import { getWeb3Assets } from '../utils/helpers'
import configureStore from 'redux-mock-store'
import defaultDrizzleOptions from '../../src/defaultOptions'
import * as DrizzleActions from '../../src/drizzleStatus/constants'
import * as ContractActions from '../../src/contracts/constants'
import * as AccountsActions from '../../src/accounts/constants'
jest.mock('../../src/DrizzleContract')
const mockDrizzleInstance = (defaultAccount, numContracts = 1) => ({
contractList: Array.from({ length: numContracts }, () => ({
options: { from: defaultAccount }
}))
})
describe('Drizzle Middleware', () => {
const accounts = global.accounts
let dmw, mockedDrizzleInstance
let next
beforeEach(() => {
mockedDrizzleInstance = mockDrizzleInstance(accounts[0], 10)
next = jest.fn()
dmw = drizzleMiddleware({ contractList: [] })
})
test('it passes action to the rest of middleware Pipeline', () => {
dmw()(next)({}) // call with undefined action
expect(next).toHaveBeenCalledTimes(1)
})
test('default sendFrom changes when wallet provider changes selectedAccount', () => {
const selectedAccount = accounts[2]
dmw()(next)({
type: DrizzleActions.DRIZZLE_INITIALIZING,
drizzle: mockedDrizzleInstance
})
dmw()(next)({ type: AccountsActions.ACCOUNTS_FETCHED, accounts: [selectedAccount] })
// All contract options should have from address set to selectedAccount
const froms = mockedDrizzleInstance.contractList.map(x => x.options.from)
expect(froms).toHaveLength(10)
const fromSet = new Set(froms)
expect(fromSet.size).toBe(1)
expect(fromSet.has(selectedAccount)).toBe(true)
expect(next).toHaveBeenCalledTimes(2)
})
test('default sendFrom does not change unnecessarily', () => {
dmw()(next)({
type: DrizzleActions.DRIZZLE_INITIALIZING,
drizzle: mockedDrizzleInstance
})
// choose 1st account to indicate no change
const selectedAccount = accounts[0]
// Sentinel remains IFF no account change is detected
const sentinel = {}
mockedDrizzleInstance.contractList.push({ options: { from: sentinel } })
dmw()(next)({ type: AccountsActions.ACCOUNTS_FETCHED, accounts: [selectedAccount] })
const froms = mockedDrizzleInstance.contractList.map(x => x.options.from)
expect(froms).toHaveLength(11)
const fromSet = new Set(froms)
expect(fromSet.size).toBe(2)
expect(fromSet.has(selectedAccount)).toBe(true)
expect(fromSet.has(sentinel)).toBe(true)
expect(next).toHaveBeenCalledTimes(2)
})
describe('dispatch AddContract', () => {
const networkId = global.defaultNetworkId
const accounts = global.accounts
const drizzleOptions = {}
const expectedDrizzleOptions = defaultDrizzleOptions
const state = { web3: { networkId }, accounts }
let middlewares, mockedStore
let drizzle, mockedWeb3, contractCreatorSpy
beforeEach(() => {
MockedDrizzleContract.mockClear()
// Mock store with middleware
middlewares = [drizzleMiddleware()]
mockedStore = configureStore(middlewares)(state)
// Mock drizzle instance and dispatch DRIZZLE_INITIALIZING
contractCreatorSpy = jest.fn()
mockedWeb3 = { eth: { Contract: contractCreatorSpy } }
drizzle = new Drizzle(drizzleOptions, mockedStore)
// Get past web3 initialization
drizzle.web3 = mockedWeb3
})
test('is initialized', () => {
const actions = mockedStore.getActions()
expect(actions).toHaveLength(1)
expect(actions[0]).toEqual({
type: DrizzleActions.DRIZZLE_INITIALIZING,
drizzle,
options: expectedDrizzleOptions
})
})
describe('Adds a Contract', () => {
const mockedContractAddress = '0x0123456789'
const mockedEvents = []
let mockedContractConfig
beforeEach(async () => {
// Arrange minimum mock of a ContractConfig
;({ truffleArtifact: mockedContractConfig } = await getWeb3Assets())
MockedDrizzleContract.mockImplementation(() => ({
contractName: mockedContractConfig.contractName
}))
mockedContractConfig.networks = { [networkId]: mockedContractAddress }
})
test('successfully', async () => {
mockedStore.dispatch({
type: ContractActions.ADD_CONTRACT,
contractConfig: mockedContractConfig,
mockedEvents
})
// Assert
const actions = mockedStore.getActions()
expect(actions).toHaveLength(4)
expect(actions[0]).toEqual({
type: DrizzleActions.DRIZZLE_INITIALIZING,
drizzle,
options: expectedDrizzleOptions
})
expect(actions[1]).toEqual({
type: ContractActions.CONTRACT_INITIALIZING,
contractConfig: mockedContractConfig
})
expect(actions[2]).toEqual({
type: ContractActions.CONTRACT_INITIALIZED,
name: mockedContractConfig.contractName
})
expect(actions[3]).toEqual({
type: ContractActions.ADD_CONTRACT,
contractConfig: mockedContractConfig,
mockedEvents
})
})
test('handles exception', async () => {
// Add a contract
const addContractAction = {
type: ContractActions.ADD_CONTRACT,
contractConfig: mockedContractConfig,
mockedEvents
}
mockedStore.dispatch(addContractAction)
const actions = mockedStore.getActions()
expect(actions).toHaveLength(4)
// Add same contract
const doppleganger = () => mockedStore.dispatch(addContractAction)
// Assert
expect(doppleganger).not.toThrow()
expect(actions).toHaveLength(5)
const errorAction = actions[4]
expect(errorAction.type).toEqual(ContractActions.ERROR_ADD_CONTRACT)
expect(errorAction.error.message).toEqual(
`Contract already exists: ${mockedContractConfig.contractName}`
)
expect(errorAction.attemptedAction).toEqual(addContractAction)
})
})
})
})

75
test/drizzle/options.test.js

@ -1,75 +0,0 @@
import { put } from 'redux-saga/effects'
import Drizzle from '../../src/Drizzle'
import defaultDrizzleOptions from '../../src/defaultOptions'
import { initializeDrizzle } from '../../src/drizzleStatus/drizzleStatusSaga'
import { NETWORK_IDS, NETWORK_MISMATCH } from '../../src/web3/constants'
describe('Drizzle options:', () => {
const accounts = global.accounts
const drizzleOptions = {}
let dispatchSpy, mockedStore, state, networkId, drizzle
beforeEach(() => {
networkId = global.defaultNetworkId
// Mock Store
state = { web3: { networkId }, accounts }
dispatchSpy = jest.fn()
mockedStore = { dispatch: dispatchSpy, getState: () => state }
})
describe('Allowed Networks:', () => {
beforeEach(() => {
drizzleOptions['networkWhitelist'] = [
NETWORK_IDS.mainnet,
NETWORK_IDS.rinkeby
]
})
test('Unauthorized network fires a mismatch', () => {
networkId = NETWORK_IDS.ropsten
drizzle = new Drizzle(drizzleOptions, mockedStore)
let next = iterateInitializeDrizzleSagaToNetworkMismatch(drizzle, drizzleOptions, networkId)
const expectedAction = put({ type: NETWORK_MISMATCH, networkId })
expect(next.value).toEqual(expectedAction)
})
test('Authorized network does NOT fire a mismatch', () => {
networkId = NETWORK_IDS.ropsten
drizzleOptions['networkWhitelist'].push(networkId)
drizzle = new Drizzle(drizzleOptions, mockedStore)
let next = iterateInitializeDrizzleSagaToNetworkMismatch(drizzle, drizzleOptions, networkId)
const unExpectedAction = put({ type: NETWORK_MISMATCH, networkId })
expect(next.value).not.toEqual(unExpectedAction)
})
test('Ganache does NOT fire a mismatch', () => {
networkId = NETWORK_IDS.ganache
drizzle = new Drizzle(drizzleOptions, mockedStore)
let next = iterateInitializeDrizzleSagaToNetworkMismatch(drizzle, drizzleOptions, networkId)
const unExpectedAction = put({ type: NETWORK_MISMATCH, networkId })
expect(next.value).not.toEqual(unExpectedAction)
})
})
})
function iterateInitializeDrizzleSagaToNetworkMismatch(drizzle, options, networkId) {
// Iterate to 3rd effect in initializeDrizzle generator
let gen = initializeDrizzle({drizzle, options})
let next = gen.next() // initializeWeb3
const fakeWeb3 = {eth: {}};
next = gen.next(fakeWeb3) // getNetworkId
// Replace saga networkId with our own
return gen.next(networkId) // networkWhitelist
}

45
test/environments/ganache-environment.js

@ -1,45 +0,0 @@
const Ganache = require('ganache-core')
const NodeEnvironment = require('jest-environment-node')
const defaultSeed = 'drizzle'
const defaultNetworkId = 6777
const defaultAccounts = [
// based on default Mnemonic
'0x8aDB46251E9cd45b5027501766531825C04a2E06',
'0xb50CF9eD8f60605bEbB967776925f21Ba5c81D5D',
'0x7fC9AD8C7A3232Aed94d6C68728D22D722694824',
'0x6DADB5b9C2510bD3C266329781adFBa9A5145442',
'0xc41E494bE83a33Bf56B5C071094859067bC9E728',
'0x5B5b5c834daCf8ad46464a283a2B1B4Bd06A456e',
'0x4B165a6036791822777C78cF7931F1d205d29118',
'0x3950A710fb4b4ed456EC469E973D35c170802609',
'0xDA343E876263D988DDD7C18Bb4aB288c7ef66D89',
'0x1Ff0eB66355D4d3A1310FB759A8a67Efd58C888A'
]
class GanacheEnvironment extends NodeEnvironment {
async setup () {
await super.setup()
// Startup a Ganache server.
this.global.provider = Ganache.provider({
seed: defaultSeed,
network_id: defaultNetworkId,
gasLimit: 7000000
})
this.global.accounts = defaultAccounts
this.global.defaultNetworkId = defaultNetworkId
// Simulate document loaded for testing drizzle
this.global.document = { readyState: 'complete' }
}
async teardown () {
// close provider engine gracefully
this.global.provider.close(() => {})
await super.teardown()
}
}
module.exports = GanacheEnvironment

67
test/generateStore.test.js

@ -1,67 +0,0 @@
import { generateStore } from '../src/generateStore'
import { getWeb3Assets } from './utils/helpers'
const partialDrizzleOptions = {
web3: {
block: false,
fallback: {
type: 'ws',
url: 'ws://127.0.0.1:9545'
}
},
polls: {
accounts: 30000
}
}
const hasBasicShape = state => {
expect(state).toHaveProperty('contracts')
expect(state).toHaveProperty('contracts.TestContract')
expect(Object.keys(state.contracts)).toHaveLength(1)
expect(state).toHaveProperty('contracts.TestContract.initialized')
expect(state).toHaveProperty('contracts.TestContract.synced')
expect(state).toHaveProperty('contracts.TestContract.storedData')
expect(state).toHaveProperty('accounts')
expect(state).toHaveProperty('accountBalances')
expect(state).toHaveProperty('currentBlock')
expect(state).toHaveProperty('drizzleStatus')
expect(state).toHaveProperty('drizzleStatus.initialized')
expect(state).toHaveProperty('transactions')
expect(state).toHaveProperty('transactionStack')
expect(state).toHaveProperty('web3')
}
describe('generateStore', () => {
let TestContract, drizzleOptions
beforeEach(async () => {
;({ truffleArtifact: TestContract } = await getWeb3Assets())
drizzleOptions = { ...partialDrizzleOptions, contracts: [TestContract] }
})
describe('has the right shape', () => {
test('when invoked with only drizzleOptions', () => {
const store = generateStore({ drizzleOptions })
const state = store.getState()
hasBasicShape(state)
})
test('when invoked with appReducer', () => {
const initialState = 'This is the initial State'
const myState = jest.fn((state = initialState) => state)
const initialAppState = { myState: initialState }
const appReducers = { myState }
const store = generateStore({
drizzleOptions,
appReducers,
initialAppState
})
const state = store.getState()
hasBasicShape(state)
expect(state).toHaveProperty('myState')
expect(state.myState).toBe(initialState)
})
})
})

42
test/utils/data/TestContract-abi.json

@ -1,42 +0,0 @@
[
{
"constant": false,
"inputs": [
{
"name": "_value",
"type": "uint256"
}
],
"name": "setData",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"name": "data",
"type": "uint256"
}
],
"name": "LogStoredData",
"type": "event"
},
{
"constant": true,
"inputs": [],
"name": "storedData",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
}
]

6
test/utils/data/TestContract-byteCode.json

@ -1,6 +0,0 @@
{
"linkReferences": {},
"object": "60806040526000805534801561001457600080fd5b5061010e806100246000396000f3fe6080604052348015600f57600080fd5b5060043610604f576000357c0100000000000000000000000000000000000000000000000000000000900480632a1afcd91460545780635b4b73a9146070575b600080fd5b605a609b565b6040518082815260200191505060405180910390f35b609960048036036020811015608457600080fd5b810190808035906020019092919050505060a1565b005b60005481565b806000819055507f1031b580b746b2e12ccbbb04a94ec78045d1f619d7194ae8863e75cd92d66116816040518082815260200191505060405180910390a15056fea165627a7a72305820c19d6e169510330ca6b40d8ed5aeecaefcb1c50aeb483b5ee99fbf5b57eaab270029",
"opcodes": "PUSH1 0x80 PUSH1 0x40 MSTORE PUSH1 0x0 DUP1 SSTORE CALLVALUE DUP1 ISZERO PUSH2 0x14 JUMPI PUSH1 0x0 DUP1 REVERT JUMPDEST POP PUSH2 0x10E DUP1 PUSH2 0x24 PUSH1 0x0 CODECOPY PUSH1 0x0 RETURN INVALID PUSH1 0x80 PUSH1 0x40 MSTORE CALLVALUE DUP1 ISZERO PUSH1 0xF JUMPI PUSH1 0x0 DUP1 REVERT JUMPDEST POP PUSH1 0x4 CALLDATASIZE LT PUSH1 0x4F JUMPI PUSH1 0x0 CALLDATALOAD PUSH29 0x100000000000000000000000000000000000000000000000000000000 SWAP1 DIV DUP1 PUSH4 0x2A1AFCD9 EQ PUSH1 0x54 JUMPI DUP1 PUSH4 0x5B4B73A9 EQ PUSH1 0x70 JUMPI JUMPDEST PUSH1 0x0 DUP1 REVERT JUMPDEST PUSH1 0x5A PUSH1 0x9B JUMP JUMPDEST PUSH1 0x40 MLOAD DUP1 DUP3 DUP2 MSTORE PUSH1 0x20 ADD SWAP2 POP POP PUSH1 0x40 MLOAD DUP1 SWAP2 SUB SWAP1 RETURN JUMPDEST PUSH1 0x99 PUSH1 0x4 DUP1 CALLDATASIZE SUB PUSH1 0x20 DUP2 LT ISZERO PUSH1 0x84 JUMPI PUSH1 0x0 DUP1 REVERT JUMPDEST DUP2 ADD SWAP1 DUP1 DUP1 CALLDATALOAD SWAP1 PUSH1 0x20 ADD SWAP1 SWAP3 SWAP2 SWAP1 POP POP POP PUSH1 0xA1 JUMP JUMPDEST STOP JUMPDEST PUSH1 0x0 SLOAD DUP2 JUMP JUMPDEST DUP1 PUSH1 0x0 DUP2 SWAP1 SSTORE POP PUSH32 0x1031B580B746B2E12CCBBB04A94EC78045D1F619D7194AE8863E75CD92D66116 DUP2 PUSH1 0x40 MLOAD DUP1 DUP3 DUP2 MSTORE PUSH1 0x20 ADD SWAP2 POP POP PUSH1 0x40 MLOAD DUP1 SWAP2 SUB SWAP1 LOG1 POP JUMP INVALID LOG1 PUSH6 0x627A7A723058 KECCAK256 0xc1 SWAP14 PUSH15 0x169510330CA6B40D8ED5AEECAEFCB1 0xc5 EXP 0xeb 0x48 EXTCODESIZE 0x5e 0xe9 SWAP16 0xbf JUMPDEST JUMPI 0xea 0xab 0x27 STOP 0x29 ",
"sourceMap": "33:209:0:-;;;87:1;61:27;;33:209;8:9:-1;5:2;;;30:1;27;20:12;5:2;33:209:0;;;;;;;"
}

12
test/utils/data/TestContract.sol

@ -1,12 +0,0 @@
pragma solidity >=0.5.0 <0.6.0;
contract TestContract {
uint public storedData = 0;
event LogStoredData(uint data);
function setData(uint _value) public {
storedData = _value;
emit LogStoredData(_value);
}
}

60
test/utils/helpers.js

@ -1,60 +0,0 @@
import Web3 from 'web3'
/**
* mockDrizzleStore
*
* @param {Object} initialState={} Set the initial State of the drizzle store.
* @returns {Array} [mockStore, dispatchedActions]
*/
const mockDrizzleStore = (initialState = {}) => {
const dispatchedActions = []
const mockStore = {
getState: () => initialState,
dispatch: action => dispatchedActions.push(action)
}
return [mockStore, dispatchedActions]
}
/**
* getWeb3
* @param {object} provider
*
* @returns {Object} A Web3 provider sourced from `global.provider`
*/
const getWeb3 = (provider = global.provider) => new Web3(provider)
/**
* getWeb3Assets deploys a contract on ganache provider
*
* @returns {Object} with web3, accounts & truffleArtifact
*/
const getWeb3Assets = async () => {
const abi = require('./data/TestContract-abi.json')
const byteCode = require('./data/TestContract-byteCode.json')
const web3 = getWeb3()
const accounts = await web3.eth.getAccounts() // use global.accounts?
const instance = new web3.eth.Contract(abi)
const deployedByteCode = await instance
.deploy({ data: byteCode.object })
.send({ from: accounts[0], gas: 150000 })
const truffleArtifact = {
contractName: 'TestContract',
abi,
byteCode,
deployedByteCode,
networks: {
[global.defaultNetworkId]: { address: deployedByteCode._address }
}
}
return { web3, accounts, truffleArtifact }
}
module.exports = {
mockDrizzleStore,
getWeb3,
getWeb3Assets
}

176
test/web3.test.js

@ -1,176 +0,0 @@
import { initializeWeb3, getNetworkId } from '../src/web3/web3Saga'
import { call, put } from 'redux-saga/effects'
import { runSaga } from 'redux-saga'
import * as Action from '../src/web3/constants'
const hasWeb3Shape = obj => {
expect(obj).toHaveProperty('currentProvider')
expect(obj).toHaveProperty('BatchRequest')
expect(obj).toHaveProperty('version')
expect(obj).toHaveProperty('utils')
expect(obj).toHaveProperty('eth')
}
describe('Resolving Web3', () => {
let web3Options, resolvedWeb3, gen
describe('with customProvider', () => {
beforeAll(async () => {
global.window = {}
web3Options = { customProvider: global.provider }
})
test('get web3', async () => {
gen = initializeWeb3(web3Options)
// First action dispatched
expect(gen.next().value).toEqual(put({ type: Action.WEB3_INITIALIZED }))
resolvedWeb3 = gen.next().value
expect(resolvedWeb3).toEqual(global.provider)
})
})
describe('with ethereum, EIP-1102 compliance', () => {
test('invokes `ethereum.enable`', async () => {
const mockedEthereumEnable = jest.fn()
const ethereum = { enable: mockedEthereumEnable }
global.window = { ethereum }
gen = initializeWeb3({})
let next = gen.next()
// get permission according to EIP 1102
//
expect(next.value).toEqual(
call({ context: ethereum, fn: ethereum.enable })
)
// return an account to simulate opt-in
next = gen.next('0x123')
expect(next.value).toEqual(put({ type: Action.WEB3_INITIALIZED }))
resolvedWeb3 = gen.next().value
hasWeb3Shape(resolvedWeb3)
})
test('loads when user opts in', async () => {
const mockedEthereumEnable = jest.fn(() => '0x123')
const ethereum = { enable: mockedEthereumEnable }
global.window = { ethereum }
const dispatched = []
const result = await runSaga({
dispatch: (action) => dispatched.push(action),
getState: () => ({ state: 'test' })
}, initializeWeb3, {}).done
// result should be a proper web3 provider
expect(result).toBeInstanceOf(require('web3'))
})
test('does not load when user opts out', async () => {
// opt out
global.window = { ethereum: { enable: jest.fn(() => undefined) } }
const dispatched = []
const web3Result = await runSaga(
{
dispatch: action => dispatched.push(action),
getState: () => ({ state: 'test' })
},
initializeWeb3,
{}
).done
// saga result should be undefined if an exception occurs
expect(web3Result).toBe(undefined)
// and the last action should be WEB3_USER_DENIED
expect(dispatched.pop()).toEqual({ type: Action.WEB3_USER_DENIED })
})
test('does not load when provider throws an error', async () => {
// simulate opting out
const mockedEthereumEnable = jest.fn(() => { throw new Error('oops') })
const ethereum = { enable: mockedEthereumEnable }
global.window = { ethereum }
const dispatched = []
const result = await runSaga({
dispatch: (action) => dispatched.push(action),
getState: () => ({ state: 'test' })
}, initializeWeb3, {}).done
// saga result is undefined when exception is thrown
expect(result).toBe(undefined)
// and the last action should be WEB3_FAILED
expect(dispatched.pop()).toEqual({ type: Action.WEB3_FAILED })
})
})
describe('with injected web3', () => {
beforeAll(async () => {
global.window = {}
global.window.web3 = { currentProvider: global.provider }
gen = initializeWeb3({})
})
test('get web3', async () => {
// First action dispatched
expect(gen.next().value).toEqual(put({ type: Action.WEB3_INITIALIZED }))
})
})
describe('with websocket fallback web3', () => {
let gen
beforeAll(async () => {
global.window = {}
global.provider.providers = { WebSocketProvider: jest.fn() }
})
test('get web3', async () => {
web3Options = {
fallback: {
type: 'ws',
url: 'ws://localhost:12345'
}
}
gen = initializeWeb3(web3Options)
// First action dispatched
expect(gen.next().value).toEqual(put({ type: Action.WEB3_INITIALIZED }))
resolvedWeb3 = gen.next().value
// is it a Web3 object?
hasWeb3Shape(resolvedWeb3)
})
test('fails when fallback type is unknown', async () => {
web3Options = {
fallback: {
type: 'thewrongtype',
url: 'ws://localhost:12345'
}
}
gen = initializeWeb3(web3Options)
const error = new Error('Invalid web3 fallback provided.')
expect(gen.next().value).toEqual(put({ type: Action.WEB3_FAILED, error }))
})
})
describe('Exhausts options', () => {
beforeAll(async () => {
global.window = {}
gen = initializeWeb3({})
})
test('with failure', async () => {
const error = new Error('Cannot find injected web3 or valid fallback.')
expect(gen.next().value).toEqual(put({ type: Action.WEB3_FAILED, error }))
})
})
})

36
types/Drizzle.d.ts

@ -1,36 +0,0 @@
import { Store } from 'redux';
import { IStoreConfig } from './generateStore';
import { IContract } from './IContract';
import { IContractConfig } from './contractStateUtils';
export interface IDrizzleOptions {
contracts: IContract[];
events?: {
[contractName: string]: any;
};
polls?: {
accounts?: number;
blocks?: number;
};
syncAlways?: any;
web3?: {
customProvider?: any;
fallback?: {
type: string;
url: string;
}
},
networkWhitelist?: number[];
}
export class Drizzle {
constructor(options?: IDrizzleOptions, store?: Store);
addContract(contractConfig: IContractConfig, events: any[]): void;
deleteContract(contractName: string): void;
findContractByAddress(address: string): IContract;
generateStore(options: IStoreConfig): Store;
}

83
types/IContract.d.ts

@ -1,83 +0,0 @@
export interface ABI {
constant?: boolean;
inputs: {
name: string;
type: string;
indexed?: boolean;
}[];
name?: string;
outputs?: {
name: string;
type: string;
}[];
payable: boolean;
stateMutability: string;
type: string;
anonymous?: boolean;
}
export interface AST {
absolutePath: string;
exportedSymbols: {
[name: string]: number[];
};
id: number;
nodeType: string;
nodes: INode[];
src: string;
}
export interface INetwork {
events: any;
links: any;
address: string;
transactionHash: string;
}
export interface INetworks {
[key: number]: INetwork;
[key: string]: INetwork;
}
export interface INode {
id: number;
literals: string[];
nodeType: string;
src: string;
baseContracts: any[];
contractDependencies: any[];
contractKind: string;
documentation?: any;
fullyImplemented?: boolean;
linearizedBaseContracts: number[];
name: string;
nodes: any[];
scope?: number;
}
export interface IContract {
contractName: string;
abi: ABI[];
metadata: string;
bytecode: string;
deployedBytecode: string;
sourceMap: string;
deployedSourceMap: string;
source: string;
sourcePath: string;
ast: AST;
legacyAST: AST;
compiler: {
name: string;
version: string;
};
networks: INetworks;
schemaVersion: string;
updatedAt: Date;
devdoc: {
methods: any;
};
userdoc: {
methods: any;
};
}

25
types/contractStateUtils.d.ts

@ -1,25 +0,0 @@
import { ABI } from "./IContract";
export interface IContractConfig {
contractName: string;
web3Contract?: {
options: {
jsonInterface: ABI;
}
};
abi?: ABI;
}
export interface IContractInitialState {
[key: string]: {};
initialized: boolean;
synced: boolean;
}
export interface IContractOptions {
contracts?: IContractConfig[];
}
export function generateContractInitialState(contractConfig: IContractConfig): IContractInitialState;
export function generateContractsInitialState(options: IContractOptions): IContractInitialState[];

13
types/generateStore.d.ts

@ -1,13 +0,0 @@
import { Store } from 'redux';
import { IDrizzleOptions } from './Drizzle';
export interface IStoreConfig {
[key: string]: any;
drizzleOptions: IDrizzleOptions;
reducers?: any;
appSagas?: any[];
appMiddlewares?: any[];
disableReduxDevTools?: boolean;
}
export function generateStore(config: IStoreConfig): Store;

3
types/index.d.ts

@ -1,3 +0,0 @@
export * from './Drizzle';
export * from './generateStore';
export * from './contractStateUtils';

30
webpack/base.config.js

@ -1,30 +0,0 @@
const path = require('path')
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
process.env.BABEL_ENV = 'production'
module.exports = {
devtool: 'inline-source-map',
entry: './src/index.js',
output: {
filename: 'drizzle-store.js',
library: '@drizzle/store',
libraryTarget: 'umd',
globalObject: "typeof self !== 'undefined' ? self : this",
path: path.resolve(__dirname, '../dist')
},
module: {
rules: [{
test: /\.(js)$/,
include: path.resolve(__dirname, '../src'),
loader: 'babel-loader'
}]
},
plugins: [
new BundleAnalyzerPlugin({
analyzerMode: 'disabled',
generateStatsFile: true,
statsOptions: { source: false }
})
]
}

6
webpack/pure.config.js

@ -1,6 +0,0 @@
const merge = require('webpack-merge');
const baseConfig = require('./base.config.js');
module.exports = merge(baseConfig, {
mode: 'production'
});

14
webpack/release.config.js

@ -1,14 +0,0 @@
const merge = require('webpack-merge');
const baseConfig = require('./base.config.js');
module.exports = merge(baseConfig, {
mode: 'development',
externals: {
'eth-block-tracker': 'eth-block-tracker-es5',
'redux': 'redux',
'redux-saga': 'redux-saga',
'web3': 'web3',
'is-plain-object': 'is-plain-object',
'deepmerge': 'deepmerge'
}
});

2744
yarn.lock

File diff suppressed because it is too large
Loading…
Cancel
Save