mirror of https://gitlab.com/ecentrics/drizzle
Ezerous
4 years ago
49 changed files with 2791 additions and 2070 deletions
@ -1,8 +0,0 @@ |
|||||
{ |
|
||||
"presets": ["@babel/preset-env"], |
|
||||
"plugins": [ |
|
||||
"@babel/plugin-transform-runtime", |
|
||||
"@babel/plugin-transform-arrow-functions", |
|
||||
"@babel/plugin-proposal-object-rest-spread" |
|
||||
] |
|
||||
} |
|
@ -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 |
|
||||
} |
|
||||
} |
|
@ -1,5 +1,12 @@ |
|||||
|
# Node |
||||
|
/node_modules |
||||
|
|
||||
|
# IDE |
||||
.DS_Store |
.DS_Store |
||||
.tern-project |
.idea |
||||
dist |
|
||||
node_modules |
# Logs |
||||
yarn* |
/log |
||||
|
npm-debug.log* |
||||
|
yarn-debug.log* |
||||
|
yarn-error.log* |
||||
|
@ -1,10 +1,9 @@ |
|||||
.DS_Store |
# Jetbrains |
||||
jest.config.js |
.idea |
||||
.travis.yml |
|
||||
.eslintrc.js |
|
||||
.babelrc |
|
||||
|
|
||||
node_modules |
# Git |
||||
test |
.gitattributes |
||||
webpack |
|
||||
.github |
# Package managers |
||||
|
yarn.lock |
||||
|
*.tgz |
||||
|
@ -1,5 +0,0 @@ |
|||||
language: node_js |
|
||||
node_js: |
|
||||
- "11.10.1" |
|
||||
- "--lts" |
|
||||
cache: npm |
|
@ -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)) |
|
@ -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:** |
|
@ -1,5 +1,7 @@ |
|||||
MIT License |
MIT License |
||||
|
|
||||
|
Copyright (c) 2020 Ezerous |
||||
|
|
||||
Copyright (c) 2017 Joshua Quintal |
Copyright (c) 2017 Joshua Quintal |
||||
|
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy |
Permission is hereby granted, free of charge, to any person obtaining a copy |
@ -1,378 +1,3 @@ |
|||||
<img src="https://truffleframework.com/img/drizzle-logo-dark.svg" width="200"> |
# @ezerous/drizzle |
||||
|
|
||||
# @drizzle/store |
A reactive data-store for web3 and smart contracts. A modified version of [@drizzle/store](https://github.com/trufflesuite/drizzle/tree/develop/packages/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) |
|
||||
|
@ -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; |
|
||||
} |
|
@ -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' |
|
||||
} |
|
@ -1,59 +1,17 @@ |
|||||
{ |
{ |
||||
"name": "@drizzle/store", |
"name": "@ezerous/drizzle", |
||||
"version": "1.5.1", |
"version": "0.1.0", |
||||
"description": "A reactive data-store for web3 and smart contracts.", |
"description": "A reactive data-store for web3 and smart contracts.", |
||||
"types": "./index.d.ts", |
"license": "MIT", |
||||
"main": "./dist/drizzle-store.js", |
"author": "Ezerous <ezerous@gmail.com>", |
||||
"react-native": "./src/index.js", |
"main": "src/index.js", |
||||
"repository": "https://github.com/trufflesuite/drizzle", |
"repository": "github:Ezerous/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" |
|
||||
}, |
|
||||
"dependencies": { |
"dependencies": { |
||||
"deepmerge": "^3.2.0", |
"deepmerge": "4.2.2", |
||||
"is-plain-object": "^2.0.4", |
"eth-block-tracker": "4.4.3", |
||||
"redux": "^4.0.1", |
"is-plain-object": "4.1.1", |
||||
"redux-saga": "^0.16.0", |
"redux": "4.0.5", |
||||
"web3": "^1.2.1" |
"redux-saga": "1.1.3", |
||||
}, |
"web3": "1.2.6" |
||||
"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" |
|
||||
} |
} |
||||
} |
} |
||||
|
Before Width: | Height: | Size: 8.9 KiB |
Before Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 13 KiB |
@ -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 })) |
|
||||
}) |
|
||||
}) |
|
@ -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 }) |
|
||||
) |
|
||||
}) |
|
||||
}) |
|
||||
}) |
|
@ -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() |
|
||||
}) |
|
||||
}) |
|
||||
}) |
|
@ -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({}) |
|
||||
}) |
|
||||
}) |
|
||||
}) |
|
@ -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) |
|
||||
}) |
|
||||
}) |
|
||||
}) |
|
@ -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) |
|
||||
}) |
|
||||
}) |
|
@ -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) |
|
||||
}) |
|
||||
}) |
|
||||
}) |
|
||||
}) |
|
@ -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
|
|
||||
} |
|
@ -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 |
|
@ -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) |
|
||||
}) |
|
||||
}) |
|
||||
}) |
|
@ -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" |
|
||||
} |
|
||||
] |
|
@ -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;;;;;;;" |
|
||||
} |
|
@ -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); |
|
||||
} |
|
||||
} |
|
||||
|
|
@ -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 |
|
||||
} |
|
@ -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 })) |
|
||||
}) |
|
||||
}) |
|
||||
}) |
|
@ -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; |
|
||||
} |
|
@ -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; |
|
||||
}; |
|
||||
} |
|
@ -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[]; |
|
@ -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; |
|
@ -1,3 +0,0 @@ |
|||||
export * from './Drizzle'; |
|
||||
export * from './generateStore'; |
|
||||
export * from './contractStateUtils'; |
|
@ -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 } |
|
||||
}) |
|
||||
] |
|
||||
} |
|
@ -1,6 +0,0 @@ |
|||||
const merge = require('webpack-merge'); |
|
||||
const baseConfig = require('./base.config.js'); |
|
||||
|
|
||||
module.exports = merge(baseConfig, { |
|
||||
mode: 'production' |
|
||||
}); |
|
@ -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' |
|
||||
} |
|
||||
}); |
|
File diff suppressed because it is too large
Loading…
Reference in new issue