DApp Development Best Practices
General
Best practices
- Use TestRPC for development and testing environments. It is simple to setup and has ether pre-filled and unlocked accounts.
- Use MetaMask for developing and testing DApps that access the blockchain directly from the UI.
- Use Solidity as the language for writing DApps.
- Use Truffle framework for DApp development.
- Use
.call(args)
when you call getter/constant function and standard calling for transactions. - Split contracts that have functionality and are data-heavy into separate ones. Read more here.
npm
- Reusable/secure helper contracts - OpenZeppelin.
Solidity pattern for returning a dynamic array of structs
You can return dynamic arrays of structs using this pattern:
pragma solidity ^0.4.15;
contract ThingRegistry {
struct Thing {
uint id;
bytes32 name;
}
Thing[] things;
function ThingRegistry() {
things.push(Thing({ id: 0, name: "thing 1" }));
things.push(Thing({ id: 1, name: "thing 2" }));
things.push(Thing({ id: 2, name: "thing 3" }));
things.push(Thing({ id: 3, name: "thing 4" }));
}
function getThings() constant returns(uint[], bytes32[]) {
uint[] memory ids = new uint[](things.length);
bytes32[] memory names = new bytes32[](things.length);
for (uint i = 0; i < things.length; ++i) {
ids[i] = things[i].id;
names[i] = things[i].name;
}
return (ids, names);
}
}
The code above shows that you can collect struct fields into multiple dynamic arrays with primitive types. And return those arrays with a multiple value return statement.
The same pattern goes for returning a single dynamic array of primitive types.
This example works if the code is called from JS and not from Solidity contracts.
Solidity pattern for extracting storage contracts
Having a data-heavy contract with functionality would be ok if you would be sure that it will never change (although, having data-heavy contracts inside the blockchain is not recommended but let’s say we really need this).
However, the contracts usually change and everytime you add/remove/modify a function, all of the data needs to be migrated to the newly deployed contract. And this is time-consuming and expensive. Since data structure changes less often than the functionality, the solution for this would be to split data and functionality into separate contracts.
Consider this simple contract:
contract Warehouse {
struct Box {
bytes32 id;
bytes32 description;
uint weight;
}
Box[] private boxes;
function addBox(bytes32 _id, bytes32 _description) public {
boxes.push(Box({ id: _id, description: _description }));
}
function updateBox(uint _index, bytes32 _description, uint _weight) public {
boxes[_index].description = _description;
boxes[_index].weight = _weight;
}
function updateBoxDescription(uint _index, bytes32 _description) public {
boxes[_index].description = _description;
}
function updateBoxWeight(uint _index, uint _weight) public {
boxes[_index].weight = _weight;
}
function getBoxCount() public constant returns(uint) {
return boxes.length;
}
function getBox(uint _index) public constant returns(bytes32, bytes32) {
return (boxes[_index].id, boxes[_index].description);
}
function doSomeFunctionality() public {
// do something
}
}
It is known that a warehouse will have a lot of boxes. So adding new functionality will force us to copy all of the boxes to the new contract.
After splitting the Warehouse
contract into two we would have the following code:
contract Warehouse {
address private storageAddress;
function setStorageAddress(address _storageAddress) public {
storageAddress = _storageAddress;
}
function getStorageAddress() public returns(address) {
return storageAddress;
}
function addBox(bytes32 _id, bytes32 _description) public {
WarehouseBoxStorage(storageAddress).addBox(_id, _description);
}
function updateBox(uint _index, bytes32 _description) public {
WarehouseBoxStorage(storageAddress).updateBox(_index, _description);
}
function getBoxCount() public constant returns(uint) {
return WarehouseBoxStorage(storageAddress).getBoxCount();
}
function getBox(uint _index) public constant returns(bytes32, bytes32) {
return WarehouseBoxStorage(storageAddress).getBox(_index);
}
function doSomeFunctionality() public {
// do something
}
}
contract WarehouseBoxStorage {
struct Box {
bytes32 id;
bytes32 description;
}
Box[] private boxes;
function addBox(bytes32 _id, bytes32 _description) {
boxes.push(Box({
id: _id,
description: _description
}));
}
function updateBox(uint _index, bytes32 _description) public {
boxes[_index].description = _description;
}
function getBoxCount() public constant returns(uint) {
return boxes.length;
}
function getBox(uint _index) public constant returns(bytes32, bytes32) {
return (boxes[_index].id, boxes[_index].description);
}
}
The API for Warehouse
contract did not change (except for two added functions) but now you can modify Warehouse
contract in any way you want without moving any of the data. What is more, we could have implemented this pattern without delegation and used WarehouseBoxStorage
directly.
Also, please note that for simplicity reasons this example does not regard any security considerations.