Drizzle DAPP with Meteor backend
For some blockchain projects we need to store information in old fashion way using application server. It can be a private client information or a BLOB that we can’t afford to store in Ethereum due to the gas cost. In this case we can use Distributed Data Protocol (DDP) that is solving a lot of problems that all developers are facing: querying a server-side database, sending the results down to the client, and then pushing changes to the client whenever anything changes in the database.
In this article we would like to show how to use Meteor backend that supports DDP in Truffle/Drizzle distributed application.
Let’s start from the DAPP side. Just unpack Drizzle box and we have running DAPP in few steps. Now we need to add a library that supports DDP protocol to our Drizzle/Truffle application. In our case we use Asteroid that is an isomorphic/universal javascript library which allows to connect to a Meteor backend from almost any JS environment. To listen our server we need to provide to an endpoint and create new Asteroid object.
const Asteroid = createClass();// Connect to a Meteor backend
const asteroid = new Asteroid({
endpoint: 'ws://localhost:9000/websocket',
});
Then we can subscribe to our collections using asteroid.subscribe function.
As our DAPP is already using a Redux store we can use it to record sate changes and update user interface if needed. For example, if a new record is added to our server collection we dispatch a new action to save the changes.
asteroid.ddp.on('added', (doc) => {
if (doc.collection === 'tasks') {
const docObj = Object.assign({}, doc.fields, { _id: doc.id });
store.dispatch(addTodo(docObj));
}
if (doc.collection === 'users') {
store.dispatch(setLoggedUser(doc.fields));
}
});
To follow Redux store rules we also need create a Reducer for our distributed collection.
const todos = (state = [], action) => {
switch (action.type) {
case ADD_TODO:
return add(state, action);
case REMOVE_TODO:
return remove(state, action);
case EDIT_TODO:
return edit(state, action);
case GET_ALL_TODO:
return action.data;
default:
return state;
}
};
and then add it to the combined reducer defined in our Drizzle box.
const reducer = combineReducers({
routing: routerReducer,
todos,
user,
...drizzleReducers
})
export default reducer
In our exampe we have HomeContainer that is wrapping our Home components and provides translation for the state to the component props. Now we need to add our distributed collection to the mapping to have access to our server data in React.
const mapStateToProps = state => {
return {
accounts: state.accounts,
SimpleStorage: state.contracts.SimpleStorage,
TutorialToken: state.contracts.TutorialToken,
drizzleStatus: state.drizzleStatus,
todos: state.todos,
user: state.user,
}
}const mapDispatchToProps = dispatch => ({
dispatchCallAddTodo: data => dispatch(callAddTodo(data)),
});Home.contextTypes = {
drizzle: PropTypes.object
}const HomeContainer = drizzleConnect(Home, mapStateToProps, mapDispatchToProps);
Plase note then we also adding mapDispatchToProps to provide Home component with ability to add a data to the collection. Then two mappings are connected to React component using drizzleConnect.
So, now we are ready to update Home component and add a logic for show our server data.
<h3>Todos</h3>
<div>
<input
type="text"
placeholder="Add todo item ..."
onKeyPress={this.handleAddTodo}
/>
</div>
<ul>
{this.props.todos.map((t, i) =>
<li key={i}>
<span className="text">
<strong>{t.data !== undefined ? t.data :'n/a'}</strong>:
{t.text}
</span>
</li>
)}
</ul>
Plase note that due to DDP we have access to server collection in our React component as props parameter and if server collection is updated then our React component will be re-rended to provide user with refreshed data.
We have connected Drizzle/Truffle DAPP with Meteor backend using Asteroid javascript libraly but it’s only one task that we need to solve. Our next goal to update DAPP subscription based on smart-contract state. For this purpose we are going to use Drizzle ‘out of the box’ SimpleStorage smart-contact.
pragma solidity ^0.4.18;contract SimpleStorage { event StorageSet( string _message ); uint public storedData; function set(uint x) public {
storedData = x;
StorageSet("Data stored successfully!");
}
}
Let’s have a look now at our backend side and integrate smart-contract logic there. Thanks to Truffle framework we can use the same artefacts that we’ve used alredy in our DAPP. Need to say that the backend also have client part running under Meteor/React control. We are not going to make transactions on server part and will use web3 library here without any key storage. Backend client part can use Truffle library with Metamask. To update React component we use withTracker function together with Tracker.Dependency object. In smart contract wrapper class we have simple getters and setters with depend and changed functions.
const contractDep = new Tracker.Dependency;class SimpleStorage {
//...
getData() {
contractDep.depend();
return this.data;
} _setData(data) {
if(this.data !== data) {
this.data = data;
contractDep.changed();
}
}
// ...
}
Now inside withTracker loop if contract data is changed then component props will be updated and then React component will be re-rendered.
class App extends Component {
// ...
export default withTracker(() => {
const update = simpleStorage.getData();
Meteor.subscribe('tasks'); return {
tasks: Tasks.find({}, { sort: { createdAt: -1 } }).fetch(),
incompleteCount: Tasks.find({
checked: { $ne: true } }).count(),
currentUser: Meteor.user(),
currentData: simpleStorage.getData(),
};
})(App);
Now we are going to update backend server part and make publication depends on the smart contract current state. On server we use web3 library to listen SimpleStorage event.
let listener = null;function setListener(cb) {
listener = cb;
}if (Meteor.isServer) {
if (typeof web3 !== 'undefined') {
web3 = new Web3(web3.currentProvider);
} else {
web3 = new Web3(new
Web3.providers.WebsocketProvider("ws://localhost:8545"));
}
const sc = new web3.eth.Contract(SimpleStorageArtifact.abi);
sc.options.address = SimpleStorageArtifact.networks[5777].address; sc.events.StorageSet({fromBlock:0},
(error, event) => { if(error)console.error(event) })
.on('data', function(event) {
sc.methods.storedData().call().then(result => {
if(listener) {
listener(result)
});
})
}
export default setListener
Now we can adjust publication for the collection
let collectionFilterfunction setFilter(filter) {
if(filter === undefined) return;
collectionFilter = filter;
}
setListener(setFilter)if (Meteor.isServer) {
Meteor.publish('tasks', function tasksPublication(data) {
return Tasks.find({
$or: [
{ private: { $ne: true } },
{ data: collectionFilter },
],
});
});
}
So we are publishing only items with data equal to collectionFilter that is SimpleStorage data. The problem here that Meteor.publish is not re-publish collection if collectionFilter is changed. Only client can push server publish and makes Task.find re-run to provide fresh data. Thanks to Tracker Meteor re-run subscribe and push re-publish if subscribe parameter is changed. So, we can update backend client subscription to get re-publish works in the following way:
export default withTracker(() => { const update = simpleStorage.getData();
Meteor.subscribe('tasks',
update !== undefined ? update.toString(10) : 'loading');
// ..}
Now we need to go back to our DAPP to make subscription works also here. First of all we are going to get update if store state is changed due to the smart-contract transaction and then if we have new state unsubscribe to the collection and subscribe again using updated SimpleStorage data value.
function _select(state) {
return state.contracts.SimpleStorage.storedData['0x0'] !== undefined ? state.contracts.SimpleStorage.storedData['0x0'].value : undefined;
}
let currentValue = 0;
let subscription = asteroid.subscribe('tasks', currentValue);function _refresh() {
let previousValue = currentValue
currentValue = _select(store.getState())
if (previousValue !== currentValue) {
asteroid.unsubscribe(subscription.id);
subscription = asteroid.subscribe('tasks', currentValue);
}
}
store.subscribe(_refresh);
Now our DAPP use SimpleStorage as a key to ‘open’ off-chain data for the client. Our backend part is listening blockchain events and as soon as a new transaction arrived open access for the client to the new content. When the client update subscription using Asteroid unsubscribe/subscribe the new content will be available in React interface.
The full code in our GitHub repository