pragma solidity >=0.4.21 <0.6.0; import 'openzeppelin-solidity/contracts/token/ERC721/ERC721Full.sol'; import 'openzeppelin-solidity/contracts/token/ERC721/ERC721MetadataMintable.sol'; import 'openzeppelin-solidity/contracts/ownership/Ownable.sol'; /** * @title Reflaunt Journey Management Contract * * this contract need to be attached to a ERC721 contract and activity contract * * only owner of a product can create a journey, * journey is a list of tasks that will be performed for a single product. * a task will produce an activity * a task should be a valid * each activity can be certified by different institution * only listed institution can only certify a specific activity * Eg. Delivery activity * EasyShip created a blockchain token for delivery * Delivery activity can be certified by Easyship token * Easyship will produce token for this particular delivery * Easyship will call Reflaunt certify method with the token id then * Easyship token id will be attached the current activity of a given Reflaunt product * journey need to respect the order of tasks which means that if a task is not completed, the journey cannot move to the next task. * there are 2 end results of a journey * - if single journey means it will not be coming back to the owner * - if return journey means it will be coming back to the owner after the last journey * 1 product can only have 1 active journey * owner can cancel a journey if it has not been performed and the product location will be back to owner address * * Eg: Single journey type => when a product is purchased by someone * Task Number | activityName | destination * Task 1 | delivery | courier || V * Task Number | activityName | destination * Task 2 | purchase | buyer * Eg: Return journey type => when a product is being serviced in a service center * Task Number | activityName | destination * Task 1 | delivery | courier || V * Task Number | activityName | destination * Task 2 | service | service center || V * Task Number | activityName | destination * Task 3 | delivery | courier */ contract Reflaunt is Ownable, ERC721Full, ERC721MetadataMintable, { enum JourneyType {SingleJourney, ReturnJourney} struct Task { string activityName; address destinationAddress; bool isPerformed; } struct Journey { bytes32 code; address ownerAddress; JourneyType type; bool isPerformed; bool isReturned; Task[] tasks; uint currentTaskIndex; bytes32 secretHash; } struct ActivityType { uint id; string name; string metadata; address[] certifyingAddresses; } struct Activity { uint activityTypeId; address destinationAddress; address[] certificationTokenAddresses; } event JourneyCreated( address indexed code, uint256 indexed productId, address indexed ownerAddress, JourneyType type, bool isPerformed, bool isReturned ); event JourneyCancelled( address indexed code, uint256 indexed productId, address indexed ownerAddress, JourneyType type, bool isPerformed, bool isReturned, uint currentTaskIndex ); event JourneyPerformed( address indexed code, uint256 indexed productId, address indexed ownerAddress, JourneyType type, bool isPerformed, bool isReturned, uint currentTaskIndex ); event JourneyReturned( address indexed code, uint256 indexed productId, address indexed ownerAddress, JourneyType type, bool isPerformed, bool isReturned, uint currentTaskIndex ); event TaskPerformed( address indexed code, uint256 indexed productId, address indexed ownerAddress, JourneyType type, bool isPerformed, bool isReturned, uint currentTaskIndex, string activityName, address destinationAddress, bool isPerformed ); modifier onlyProductOwner(bytes32 _productId) { require(super.ownerOf(_productId) == msg.sender, "sender is not the owner a the product"); _; } modifier productHasNoActiveJourney(bytes32 _productId) { require(_productJourney[_productId].id != 0, "there is still active journey for the product"); _; } modifier productHasActiveJourney(bytes32 _productId) { require(_productJourney[_productId].id == 0, "there is no active journey for the product"); _; } modifier hasCorrectJourneyCode(bytes32 _productId, bytes32 hashCode) { require(_productJourney[_productId].code == hashCode, "there is no active journey for the product"); _; } modifier isDesignatedAddress(bytes32 _productId) { if (_productJourney[_productId].type == JourneyType.SingleJourney) { // TODO: allow an on behalf address of msg.sender, do checking here require(_productJourney[_productId].tasks[_productJourney[_productId].currentTaskIndex].destinationAddress == msg.sender, "sender is not authorized for this task"); } else { if (_productJourney[_productId].isPerformed == false) { require(_productJourney[_productId].tasks[_productJourney[_productId].currentTaskIndex].destinationAddress == msg.sender, "sender is not authorized for this task"); } else { require(_productJourney[_productId].ownerAddress == msg.sender, "sender is not authorized for this task"); } } _; } mapping(uint256 => Journey) private _productJourney; mapping(uint256 => address) private _productLocation; mapping(uint => ActivityType) private _activityTypes; mapping(uint256 => Activity[]) private _productActivityLogs; constructor() ERC721Full("Reflaunt", "RF") public { } /** * @dev Create a new journey if there is no journey * this will returns a journey hash. * to perform a task, the respective destination will need to input journey hash * journey hash will be different after a task is performed * * @param _productId id of a product token which the journey will be performed to * @param _journeyType Type of the journey can be single or return journey * @param _secretHash Hash of a secret * @param _tasks List of tasks that will be included inside the journey * @return journey hash */ function createJourney(uint256 _productId, JourneyType _type, bytes32 _secretHash, Task[] _tasks) external onlyProductOwner(_productId) productHasNoActiveJourney(_productId) returns (bytes32){ require(_secretHash != 0); require(_type != 0); for (uint i = 0; i < _tasks.length; i++) { require(_tasks[i].destinationAddress != address(0)); // TODO: verify activityName available later when we have list of activities } bytes32 _code = sha256(abi.encodePacked(_productId, msg.sender, _type, _secretHash, 0)); _productJourney[_productId] = Journey( { ownerAddress: msg.sender, code : _code, type : _type, isPerformed : false, isReturned : false, tasks : _tasks, currentTaskIndex : 0, secretHash : _secretHash } ); emit JourneyCreated(_code, _productId, msg.sender, _type, false, false, 0); return _code; } /** * @dev Cancel an active journey * * @param _productId of a product where we want to cancel journey * @return journey hash */ function cancelJourney(uint256 _productId) external onlyProductOwner(_productId) productHasActiveJourney(_productId) { emit JourneyCancelled( _productJourney[_productId].code, _productId, msg.sender, _productJourney[_productId].type, _productJourney[_productId].isPerformed, _productJourney[_productId].isReturned, _productJourney[_productId].currentTaskIndex); _productJourney[_productId] = Journey(); _productLocation[_productId] = msg.sender; } /** * @dev Perform a task inside a journey * * @param _hashCode hash code of a task * @param _productId id of a product token which the journey will be performed to * @return next journey hash */ function performTask(bytes32 _hashCode, uint256 _productId) external hasCorrectJourneyCode(_productId, _hashCode) isDesignatedAddress(_productId) returns(bytes32){ emit TaskPerformed( _productJourney[_productId].code, _productId, msg.sender, _productJourney[_productId].type, _productJourney[_productId].isPerformed, _productJourney[_productId].isReturned, _productJourney[_productId].currentTaskIndex, _productJourney[_productId].tasks[_productJourney[_productId].currentTaskIndex].activityName, _productJourney[_productId].tasks[_productJourney[_productId].currentTaskIndex].destinationAddress, _productJourney[_productId].tasks[_productJourney[_productId].currentTaskIndex].isPerformed); _productActivityLogs[_productId].push(Activity ({ activityTypeId: _productJourney[_productId].tasks[_productJourney[_productId].currentTaskIndex].activityName destinationAddress: _productJourney[_productId].tasks[_productJourney[_productId].currentTaskIndex].destinationAddress })); if (_productJourney[_productId].type == JourneyType.ReturnJourney) { if (_productJourney[_productId].isPerformed == true) { _productJourney[_productId].isReturned = true; emit JourneyReturned( _productJourney[_productId].code, _productId, msg.sender, _productJourney[_productId].type, _productJourney[_productId].isPerformed, _productJourney[_productId].isReturned, _productJourney[_productId].currentTaskIndex); // after owner claim back, the last task, we reset the product journey cancelJourney(_productId); return 0; } } if (_productJourney[_productId].currentTaskIndex == _productJourney[_productId].tasks.length - 1) { _productJourney[_productId].isPerformed = true; if (_productJourney[_productId].type == JourneyType.SingleJourney) { emit JourneyPerformed( _productJourney[_productId].code, _productId, msg.sender, _productJourney[_productId].type, _productJourney[_productId].isPerformed, _productJourney[_productId].isReturned, _productJourney[_productId].currentTaskIndex); _productLocation[_productId] = msg.sender; super._transferFrom(_productJourney[_productId].ownerAddress, msg.sender, _productId); _productJourney[_productId] = Journey(); return 0; } } _productJourney[_productId].currentTaskIndex++; bytes32 _code = sha256(abi.encodePacked( _productId, _productJourney[_productId].ownerAddress, _productJourney[_productId].type, _productJourney[_productId].secretHash, _productJourney[_productId].currentTaskIndex)); _productJourney[_productId].code = _code; _productLocation[_productId] = msg.sender; return _code; } function newActivity(uint _id, string _name, string _metadata, address[] _certifyingAddresses) external onlyOwner() returns (uint){ require(_id != 0, 'id cannot be 0'); require(_activityTypes[_id] != 0, 'activity with given id is exist'); _activityTypes[_id] = ActivityType({ id: _id, name: _name, metadata: _metadata, certifyingAddresses: _certifyingAddresses }); return _id; } function addCertifyingAddress(uint _id, address _certifyingAddress) external onlyOwner() { require(_activityTypes[_id] == 0, 'activity with given id not exist'); _activityTypes[_id].certifyingAddresses.push(_certifyingAddress); } function certify(uint256 _productId, address _tokenId) external { require(_productActivityLogs[_productId].length > 0, 'no active activity'); require(msg.sender == _productActivityLogs[_productId][_productActivityLogs[_productId].length - 1]); address[] addr = _activityTypes[_productActivityLogs[_productId][_productActivityLogs[_productId].length - 1].activityTypeId].certifyingAddresses; bool isAllowed = false; for (uint i = 0; i < addr.length; i++) { if(addr[i] == msg.sender) { isAllowed = true; } } require(isAllowed == true); _productActivityLogs[_productId][_productActivityLogs[_productId].length - 1].certificationTokenAddresses.push(_tokenId); } }