diff --git a/contracts/TinyHops.sol b/contracts/TinyHops.sol index 62a1dfa..9ee14a6 100644 --- a/contracts/TinyHops.sol +++ b/contracts/TinyHops.sol @@ -1,5 +1,18 @@ // SPDX-License-Identifier: GPLv3 pragma solidity ^0.8.6; +import "forge-std/console.sol"; +import "./library/LibTinyHopsTemplateResolver.sol"; + +uint256 constant WORKFLOW_STATUS_NOT_STARTED = 0; +uint256 constant WORKFLOW_STATUS_STARTED = 1; +uint256 constant WORKFLOW_STATUS_PAUSED = 2; +uint256 constant WORKFLOW_STATUS_COMPLETED = 3; +uint256 constant WORKFLOW_STATUS_FAILED = 4; + +uint256 constant STEP_STATUS_NOT_STARTED = 0; +uint256 constant STEP_STATUS_STARTED = 1; +uint256 constant STEP_STATUS_PAUSED = 2; +uint256 constant STEP_STATUS_COMPLETED = 3; // give it the ModicumContract interface // NB: this will be a separate import in future. @@ -11,9 +24,10 @@ interface ModicumContract { } struct WorkflowEntry { + uint256 cost; uint256 stepId; string cmd; - string params; + bytes params; uint256[] deps; } @@ -27,9 +41,35 @@ contract TinyHops { // simple ownership mapping mapping(uint256 => address) workflowIdToOwner; + // status of the workflow + // 0 - not started + // 1 - started + // 2 - paused + // 3 - completed + // 4 - failed mapping(uint256 => uint256) workflowIdToStatus; mapping(uint256 => Workflow) workflowIdToWorkflow; mapping(uint256 => uint256) workflowIdToBalance; + mapping(uint256 => uint256) workflowIdToJobId; + mapping(uint256 => uint256) jobIdToWorkflowId; + // entry is a sha256 hash of the workflowId and stepId resulting in a single tiered lookup + mapping(bytes32 => string) workflowStepHashToCid; + + mapping(uint256 => uint256[2]) jobIdToStepIndex; + mapping(uint256 => uint256) jobIdToStepId; + + mapping(uint256 => mapping(uint256 => string)) workflowIdToResultCid; + + // This takes a sha256 hash of the job_id and the step_index to mark the step as received completed + //3 is failed 2 is complete 1 is pending 0 is not complete + + mapping(bytes32 => uint256) stepHashToStepStatus; + mapping(bytes32 => string) stepHashToStepResult; + // the job id tracks the iteration in the workflow entries + // this is the counter to track your position in the workflow, + // if you have to pause to reset or resume this will track your position + // and start executing from there again + mapping(uint256 => uint256) jobIdToWorkflowCounter; // auto increment workflow id uint256 public workflowIdCounter; @@ -45,6 +85,13 @@ contract TinyHops { uint256 public job1; uint256 public job2; + event WorkflowAdded(uint256 workflowId, address owner); + event WorkflowStartedJob( + uint256 workflowId, + uint256 jobId, + uint256 balance + ); + // The Modicum contract address is found here: https://github.com/bacalhau-project/lilypad-modicum/blob/main/latest.txt // Current: 0x422F325AA109A3038BDCb7B03Dd0331A4aC2cD1a constructor(address _modicumContract) { @@ -55,55 +102,385 @@ contract TinyHops { contractAddress = _modicumContract; //make a connection instance to the remote contract remoteContractInstance = ModicumContract(_modicumContract); - workflowIdCounter = 1; + } + + function increaseWorkflowIdCounter() internal returns (uint256) { + workflowIdCounter++; + return workflowIdCounter; + } + + function ownerOf(uint256 workflowId) public view returns (address) { + return workflowIdToOwner[workflowId]; + } + + function setWorkFlowStatus( + uint256 workflowId, + uint256 status + ) internal returns (uint256) { + workflowIdToStatus[workflowId] = status; + return workflowIdToStatus[workflowId]; + } + + function getWorkFlowStatus( + uint256 workflowId + ) public view returns (uint256) { + return workflowIdToStatus[workflowId]; + } + + function storeWorkflow(Workflow memory workflow) public returns (uint256) { + uint256 workflowId = increaseWorkflowIdCounter(); + + // Get a storage reference to the new workflow + Workflow storage newWorkflow = workflowIdToWorkflow[workflowId]; + + // Iterate through the outer array + for (uint256 i = 0; i < workflow.entries.length; i++) { + newWorkflow.entries.push(); // Create a new inner array in storage + WorkflowEntry[] storage entriesStorage = newWorkflow.entries[ + newWorkflow.entries.length - 1 + ]; + + // Iterate through the inner array + for (uint256 j = 0; j < workflow.entries[i].length; j++) { + WorkflowEntry memory entryMem = workflow.entries[i][j]; + + // Create a new entry in storage + entriesStorage.push(); + WorkflowEntry storage entryStorage = entriesStorage[ + entriesStorage.length - 1 + ]; + + // Copy data from memory to storage + entryStorage.stepId = entryMem.stepId; + entryStorage.cmd = entryMem.cmd; + entryStorage.params = entryMem.params; + entryStorage.cost = entryMem.cost; + + // Copy deps array from memory to storage + /*for (uint256 k = 0; k < entryMem.deps.length; k++) { + entryStorage.deps.push(entryMem.deps[k]); + }*/ + } + } + workflowIdToOwner[workflowIdCounter] = msg.sender; + + return workflowId; + } + + modifier isWorkflowOwner(uint256 workflowId) { + require( + msg.sender == workflowIdToOwner[workflowId], + "Only the owner can call this function" + ); + _; + } + + function creditBalance(uint256 workflowId, uint256 amount) internal { + workflowIdToBalance[workflowId] = + workflowIdToBalance[workflowId] + + amount; } function startWorkflow( - Workflow memory workflow - ) public payable returns (uint256) { + uint256 workflowId + ) public payable isWorkflowOwner(workflowId) returns (uint256) { require( msg.value >= 5 ether, "Payment of at least 5 Ether per step is required maybe more" ); - workflowIdToOwner[workflowIdCounter] = msg.sender; - workflowIdToWorkflow[workflowIdCounter] = workflow; - workflowIdToBalance[workflowIdCounter] = - msg.value + - workflowIdToBalance[workflowIdCounter]; + // Allocate storage for the new workflow + // Workflow storage workflow = workflowIdToWorkflow[workflowId]; + require( + workflowIdToStatus[workflowId] == 0, + "Workflow already started" + ); + creditBalance(workflowId, msg.value); + executeNextStep(workflowId, getNextStep(workflowId)); - // kick off the first workflow steps + /*for (uint256 i = 0; i < workflow.entries[0].length; i++) { + /* require(workflow.entries[0][0].cost > 0, "cost is zero"); + require( + workflow.entries[0][0].cost <= workflowIdToBalance[workflowId], + "not enough funds" + + // set the balance less the cost of running + workflowIdToBalance[workflowId] = + workflowIdToBalance[workflowId] - + workflow.entries[0][i].cost; - return ++workflowIdCounter; - } + uint256 jobId = remoteContractInstance + .runModuleWithDefaultMediators{ + value: workflow.entries[0][i].cost + }(workflow.entries[0][i].cmd, workflow.entries[0][i].params); + + jobIdToWorkflowId[jobId] = workflowId; + jobIdToStepIndex[jobId][0] = 0; + jobIdToStepIndex[jobId][1] = i; + jobIdToWorkflowCounter[jobId] = 0; + // kick off the first workflow steps + emit WorkflowStartedJob( + workflowId, + jobId, + workflowIdToBalance[workflowIdCounter] + ); + */ + workflowIdToStatus[workflowId] = 1; - function runBatchSDXL( - string memory prompt1, - string memory prompt2 - ) public payable { - job1 = remoteContractInstance.runModuleWithDefaultMediators{ - value: msg.value / 2 - }("sdxl:v0.9-lilypad1", prompt1); - job2 = remoteContractInstance.runModuleWithDefaultMediators{ - value: msg.value / 2 - }("sdxl:v0.9-lilypad1", prompt2); + return workflowId; } /* * @notice Run the SDXL Module * @param prompt The input text prompt to generate the stable diffusion image from - */ function runSDXL(string memory prompt) public payable returns (uint256) { //require(msg.value == 2 ether, "Payment of 2 Ether is required"); //all jobs are currently 2 lilETH return remoteContractInstance.runModuleWithDefaultMediators{ value: msg.value }("sdxl:v0.9-lilypad1", prompt); + }*/ + function getWorkHash( + uint256 _workflowId, + uint256 _stepId + ) public pure returns (bytes32) { + return keccak256(abi.encodePacked(_workflowId, _stepId)); + } + + function _getStepIndices( + uint256 _jobId + ) internal returns (uint256[2] memory) { + uint256 workflowId = jobIdToWorkflowId[_jobId]; + uint256 lastKnownWorkflowIndex = jobIdToWorkflowCounter[_jobId]; + uint256 workflowIndex = jobIdToStepIndex[_jobId][0]; + uint256 stepIndex = jobIdToStepIndex[_jobId][1]; + return [workflowIndex, stepIndex]; + } + + function _updateStepStatus( + uint256 _jobId, + string calldata _cid, + uint256 _status + ) internal returns (uint256) { + // bytes32 workHash = getWorkHash(_jobId, _workflowIndex, _stepId); + uint256 workflowId = jobIdToWorkflowId[_jobId]; + uint256 _stepId = jobIdToStepId[_jobId]; + bytes32 workHash = getWorkHash(workflowId, _stepId); + stepHashToStepStatus[workHash] = _status; + stepHashToStepResult[workHash] = _cid; + workflowIdToResultCid[workflowId][_stepId] = _cid; + return workflowId; + } + + function updateBalance(uint256 workflowId, uint256 cost) internal { + require( + workflowIdToBalance[workflowId] >= cost, + "not enough funds to run" + ); + workflowIdToBalance[workflowId] = + workflowIdToBalance[workflowId] - + cost; + } + + function getBalance(uint256 workflowId) public view returns (uint256) { + return workflowIdToBalance[workflowId]; + } + + function getNextStep(uint256 workflowId) internal view returns (uint256) { + Workflow storage workflow = workflowIdToWorkflow[workflowId]; + uint256 numEntries = workflow.entries.length; + for (uint256 i = 0; i < numEntries; i++) { + bool isCompleted = true; + uint256 numSubEntries = workflow.entries[i].length; + for (uint256 j = 0; j < numSubEntries; j++) { + bytes32 workHash = getWorkHash( + workflowId, + workflow.entries[i][j].stepId + ); + if (stepHashToStepStatus[workHash] == 0) { + isCompleted = false; + break; + } + } + if (!isCompleted) { + return i; + } + } + return numEntries; + } + + function executeNextStep( + uint256 workflowId, + uint256 workflowIndex + ) internal { + Workflow storage workflow = workflowIdToWorkflow[workflowId]; + + uint256 numEntries = workflow.entries[workflowIndex].length; + for (uint256 i = 0; i < numEntries; i = i + 1) { + // This will also throw on underflow must have non zero balance at all times + updateBalance(workflowId, workflow.entries[workflowIndex][i].cost); + + uint256 jobId = remoteContractInstance + .runModuleWithDefaultMediators{ + value: workflow.entries[workflowIndex][i].cost + }( + workflow.entries[workflowIndex][i].cmd, + LibTinyHopsTemplateResolver.applyVariables( + workflow.entries[workflowIndex][i].params, + workflowIdToResultCid[workflowId] + ) + ); + jobIdToWorkflowId[jobId] = workflowId; + jobIdToStepId[jobId] = workflow.entries[workflowIndex][i].stepId; + stepHashToStepStatus[ + getWorkHash( + workflowId, + workflow.entries[workflowIndex][i].stepId + ) + ] = STEP_STATUS_STARTED; + // jobIdToStepIndex[jobId][0] = workflowIndex; + // jobIdToStepIndex[jobId][1] = i; + jobIdToWorkflowCounter[jobId] = workflowIndex; + emit WorkflowStartedJob( + workflowId, + jobId, + workflowIdToBalance[workflowIdCounter] + ); + } + return; + } + + function getEntryParam( + uint256 workflowId, + WorkflowEntry memory entry + ) internal view returns (string memory) { + return + LibTinyHopsTemplateResolver.applyVariables( + entry.params, + workflowIdToResultCid[workflowId] + ); + } + + function getWorkFlowResults( + uint256 workflowId + ) + public + view + returns (uint256[] memory, string[] memory, string[] memory params) + { + Workflow storage workflow = workflowIdToWorkflow[workflowId]; + uint256 numEntries = 0; + for (uint256 i = 0; i < workflow.entries.length; i++) { + numEntries += workflow.entries[i].length; + } + uint256[] memory stepIds = new uint256[](numEntries); + string[] memory cids = new string[](numEntries); + string[] memory params = new string[](numEntries); + + uint256 index = 0; + for (uint256 i = 0; i < workflow.entries.length; i++) { + for (uint256 j = 0; j < workflow.entries[i].length; j++) { + stepIds[index] = workflow.entries[i][j].stepId; + cids[index] = stepHashToStepResult[ + getWorkHash(workflowId, workflow.entries[i][j].stepId) + ]; + params[index] = getEntryParam( + workflowId, + workflow.entries[i][j] + ); + index++; + } + } + + return (stepIds, cids, params); + } + + function getStepStatus( + uint256 workflowId, + uint256 stepId + ) public view returns (uint256) { + bytes32 workHash = getWorkHash(workflowId, stepId); + return stepHashToStepStatus[workHash]; + } + + function checkAllEntryStepsComplete( + uint256 _workflowId + ) public view returns (bool) { + Workflow storage workflow = workflowIdToWorkflow[_workflowId]; + for (uint256 i = 0; i < workflow.entries.length; i++) { + for (uint256 j = 0; j < workflow.entries[i].length; j++) { + bytes32 workHash = getWorkHash( + _workflowId, + workflow.entries[i][j].stepId + ); + if (stepHashToStepStatus[workHash] != STEP_STATUS_COMPLETED) { + return false; + } + } + } + return true; + } + + function checkPreviousLevelComplete( + uint256 workflowID, + uint256 workflowIndex + ) public returns (bool) { + uint256 prevWorkflowIndex; + if (workflowIndex != 0) { + prevWorkflowIndex = workflowIndex - 1; + } + Workflow storage workflow = workflowIdToWorkflow[workflowID]; + if (prevWorkflowIndex >= workflow.entries.length) { + // If the previous workflow index is out of bounds, then we can assume it's not complete + return false; + } + uint256 numEntries = workflow.entries[prevWorkflowIndex].length; + for (uint256 i = 0; i < numEntries; i++) { + bytes32 workHash = getWorkHash( + workflowID, + workflow.entries[prevWorkflowIndex][i].stepId + ); + if (stepHashToStepStatus[workHash] != STEP_STATUS_COMPLETED) { + return false; + } + } + return true; } // This must be implemented in order to receive the job results back! function receiveJobResults(uint256 _jobID, string calldata _cid) public { - if (_jobID == job1) { + uint256 workflowId = _updateStepStatus( + _jobID, + _cid, + STEP_STATUS_COMPLETED + ); + if (checkAllEntryStepsComplete(workflowId)) { + setWorkFlowStatus(workflowId, WORKFLOW_STATUS_COMPLETED); + return; + } + uint256 workflowIndex = getNextStep(workflowId); + if (checkPreviousLevelComplete(workflowId, workflowIndex)) { + // returns last operating cache for the list + executeNextStep(workflowId, workflowIndex); + } + // should update the status and store the result + /* (uint256 workflowID, uint256 workflowIndex) = _updateStepStatus( + jobID, + _cid + ); + // uint256[2] memory stepIndices = _getStepIndices(_jobID); + // _updateStepStatus(_jobID, stepIndices[0], stepIndices[1], STEP_STATUS_COMPLETED); + if (checkAllEntryStepsComplete(workflowID, workflowIndex)) { + workflowIndex = increaseWorkflowCounter(workflowId); + } + if (checkMoreWork(workflowID, workflowIndex)) { + // workflowIndex ++; + executeNextStep(workflowID, workflowIndex); + } else { + setWorkflowStatus(workflowID, WORKFLOW_STATUS_COMPLETED); + } * + } + /* if (_jobID == job1) { resultJobId1 = _jobID; resultCIDs1 = _cid; } @@ -111,5 +488,6 @@ contract TinyHops { resultJobId2 = _jobID; resultCIDs2 = _cid; } + } */ } } diff --git a/contracts/library/LibTinyHopsTemplateResolver.sol b/contracts/library/LibTinyHopsTemplateResolver.sol index e637a35..6b9e2ba 100644 --- a/contracts/library/LibTinyHopsTemplateResolver.sol +++ b/contracts/library/LibTinyHopsTemplateResolver.sol @@ -72,7 +72,6 @@ library LibTinyHopsTemplateResolver { stepId := shr(192, mload(ptr)) } ptr += 8; // Move past the 8-byte stepId - result = string(abi.encodePacked(result, replacements[stepId])); } } diff --git a/test/foundry/ModicumMock.t.sol b/test/foundry/ModicumMock.t.sol index 81bdefa..f36c48b 100644 --- a/test/foundry/ModicumMock.t.sol +++ b/test/foundry/ModicumMock.t.sol @@ -1,13 +1,30 @@ -// SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import "forge-std/Test.sol"; contract ModicumMockTest is Test { - uint256 public currentJob; + uint256[] public currentJobs; + mapping(uint256 => string) jobParams; + + function getCurrentJobs() public view returns (uint256[] memory) { + return currentJobs; + } function clearJobIds() public { - currentJob = 0; + uint256 numKeys = currentJobs.length; + for (uint256 i = 0; i < numKeys; i++) { + delete jobParams[i]; + } + numKeys = 0; + delete currentJobs; + } + + function getBalance() public view returns (uint256) { + return address(this).balance; + } + + function getParams(uint256 jobId) public view returns (string memory) { + return jobParams[jobId]; } function runModuleWithDefaultMediators( @@ -15,7 +32,10 @@ contract ModicumMockTest is Test { string calldata params ) external payable returns (uint256) { // use sha256 to also generate a uint256 number - return getJobId(name, params, ++currentJob); + uint256 currentJobId = getJobId(name, params, currentJobs.length); + jobParams[currentJobId] = params; + currentJobs.push(currentJobId); + return currentJobId; } function getJobId( diff --git a/test/foundry/TinyHops.t.sol b/test/foundry/TinyHops.t.sol index f73347d..d051381 100644 --- a/test/foundry/TinyHops.t.sol +++ b/test/foundry/TinyHops.t.sol @@ -20,20 +20,28 @@ contract TinyHopsTest is Test { tinyHops = new TinyHops(address(modicumMock)); } + function buildWorkflowParamVar( + uint64 stepId + ) public view returns (bytes memory) { + return abi.encodePacked(uint16(1), uint64(stepId)); + } + + function buildWorkflowParamText( + string memory text + ) public view returns (bytes memory) { + return + abi.encodePacked( + uint16(0), + uint64(bytes(text).length), + bytes(text) + ); + } + function getTestStream() public view returns (bytes memory testStream) { - bytes memory hello = abi.encodePacked( - uint16(0), - uint64(6), - bytes("hello ") - ); - bytes memory space = abi.encodePacked(uint16(0), uint64(1), bytes(" ")); - bytes memory world = abi.encodePacked( - uint16(0), - uint64(7), - bytes(" world ") - ); - bytes memory stepId0 = abi.encodePacked(uint16(1), uint64(0)); - bytes memory stepId1 = abi.encodePacked(uint16(1), uint64(1)); + bytes memory hello = buildWorkflowParamText("hello "); + bytes memory world = buildWorkflowParamText(" world "); + bytes memory stepId0 = buildWorkflowParamVar(0); + bytes memory stepId1 = buildWorkflowParamVar(1); testStream = abi.encodePacked(hello, stepId0, world, stepId1); } @@ -56,15 +64,223 @@ contract TinyHopsTest is Test { } function testSingleJob() public { + vm.deal(user1, 10 ether); vm.startPrank(user1); + Workflow memory workflow; + workflow.entries = new WorkflowEntry[][](1); + workflow.entries[0] = new WorkflowEntry[](1); + // generate a workflow Entry + workflow.entries[0][0] = WorkflowEntry( + 5 ether, + 0, + "cowsay:v0.0.1", + buildWorkflowParamText("hello world"), + new uint256[](0) + ); + uint256 workflowId = tinyHops.storeWorkflow(workflow); + assertEq(workflowId, 1); + assertEq(user1, tinyHops.ownerOf(workflowId)); + tinyHops.startWorkflow{value: 5 ether}(workflowId); + assertEq( + WORKFLOW_STATUS_STARTED, + tinyHops.getWorkFlowStatus(workflowId) + ); + vm.stopPrank(); + vm.startPrank(address(modicumMock)); + uint256[] memory currentJobs = modicumMock.getCurrentJobs(); + assertEq(tinyHops.getStepStatus(workflowId, 0), STEP_STATUS_STARTED); + assertEq(currentJobs.length, 1); + assertEq(modicumMock.getParams(currentJobs[0]), "hello world"); + tinyHops.receiveJobResults(currentJobs[0], "fake cid"); + assertEq(tinyHops.getStepStatus(workflowId, 0), STEP_STATUS_COMPLETED); + ( + uint256[] memory stepIds, + string[] memory cids, + string[] memory params + ) = tinyHops.getWorkFlowResults(workflowId); + assertEq(stepIds[0], 0); + assertEq(cids[0], "fake cid"); + assertEq( + tinyHops.getWorkFlowStatus(workflowId), + WORKFLOW_STATUS_COMPLETED + ); // uint256 workflowId = tinyHops.startWorkflow(workflow); // tinyHops.getWorkflow(workflowId); } - function testSequentialJob() public {} + // Parallel Job test shoudl work we also permuted the order of the jobs, + // so out of order execution should work as well async comms and all - function testParallelJob() public {} + /* + This job will have 3 steps a starting job + two parallel jobs + and a final job that aggregates thte results of the parallel job + each job will feed the results into the next job, the final job will also + take results of the first job in into account as well in it's parameter + demonstrating the ability to process a complex DAG graph workflow + + + + */ + + function testFanOutInJob() public { + vm.deal(user1, 40 ether); + vm.startPrank(user1); + Workflow memory workflow; + workflow.entries = new WorkflowEntry[][](3); + workflow.entries[0] = new WorkflowEntry[](1); + workflow.entries[1] = new WorkflowEntry[](2); + workflow.entries[2] = new WorkflowEntry[](1); + // generate a workflow Entry + workflow.entries[0][0] = WorkflowEntry( + 5 ether, + 0, + "cowsay:v0.0.1", + buildWorkflowParamText("hello world l0"), + new uint256[](0) + ); + + workflow.entries[1][0] = WorkflowEntry( + 5 ether, + 1, + "cowsay:v0.0.1", + //"hello world l1(0,0) {{stepId: 0}}", + abi.encodePacked( + buildWorkflowParamText("hello world l1(0,0) "), + buildWorkflowParamVar(0) + ), + new uint256[](0) + ); + + workflow.entries[1][1] = WorkflowEntry( + 5 ether, + 2, + "cowsay:v0.0.1", + //"hello world l1(0,1) {{stepId: 0}}", + abi.encodePacked( + buildWorkflowParamText("hello world l1(0,1) "), + buildWorkflowParamVar(0) + ), + new uint256[](0) + ); + + workflow.entries[2][0] = WorkflowEntry( + 5 ether, + 3, + "cowsay:v0.0.1", + //"{{stepId: 0}} hello world {{stepId: 1}} l2(0,0) {{stepId: 2}}", + abi.encodePacked( + buildWorkflowParamVar(0), + buildWorkflowParamText(" hello world "), + buildWorkflowParamVar(1), + buildWorkflowParamText(" l2(0,0) "), + buildWorkflowParamVar(2) + ), + new uint256[](0) + ); + uint256 workflowId = tinyHops.storeWorkflow(workflow); + assertEq(workflowId, 1); + assertEq(user1, tinyHops.ownerOf(workflowId)); + tinyHops.startWorkflow{value: 30 ether}(workflowId); + assertEq( + WORKFLOW_STATUS_STARTED, + tinyHops.getWorkFlowStatus(workflowId) + ); + vm.stopPrank(); + vm.startPrank(address(modicumMock)); + uint256[] memory currentJobs = modicumMock.getCurrentJobs(); + console.log("len", currentJobs.length); + + assertEq(tinyHops.getStepStatus(workflowId, 0), STEP_STATUS_STARTED); + assertEq(currentJobs.length, 1); + assertEq(modicumMock.getParams(currentJobs[0]), "hello world l0"); + tinyHops.receiveJobResults(currentJobs[0], "fake cid 0"); + currentJobs = modicumMock.getCurrentJobs(); + assertEq(currentJobs.length, 3); + tinyHops.receiveJobResults(currentJobs[1], "fakecid1"); + currentJobs = modicumMock.getCurrentJobs(); + assertEq(currentJobs.length, 3); + tinyHops.receiveJobResults(currentJobs[2], "fakecid2"); + currentJobs = modicumMock.getCurrentJobs(); + assertEq(currentJobs.length, 4); + tinyHops.receiveJobResults(currentJobs[3], "fakecid3"); + ( + uint256[] memory stepIds, + string[] memory cids, + string[] memory params + ) = tinyHops.getWorkFlowResults(workflowId); + assertEq(params[3], "fake cid 0 hello world fakecid1 l2(0,0) fakecid2"); + assertEq(tinyHops.getBalance(workflowId), 10 ether); + assertEq(modicumMock.getBalance(), 20 ether); + //console.log(balance(tinyHops)); + // Step should show as started cause it hasn't completed + /* assertEq(tinyHops.getStepStatus(workflowId, 0), STEP_STATUS_STARTED); + + // Step should show completed + assertEq(tinyHops.getStepStatus(workflowId, 1), STEP_STATUS_COMPLETED); + assertEq( + WORKFLOW_STATUS_STARTED, + tinyHops.getWorkFlowStatus(workflowId) + ); + + tinyHops.receiveJobResults(currentJobs[0], "fake cid 0"); + (uint256[] memory stepIds, string[] memory cids) = tinyHops + .getWorkFlowResults(workflowId); + assertEq(stepIds[0], 0); + assertEq(cids[0], "fake cid 0"); + assertEq(stepIds[1], 1); + assertEq(cids[1], "fake cid 1"); + assertEq( + tinyHops.getWorkFlowStatus(workflowId), + WORKFLOW_STATUS_COMPLETED + ); + */ + } + + function testSequentialJob() public { + vm.deal(user1, 10 ether); + vm.startPrank(user1); + Workflow memory workflow; + workflow.entries = new WorkflowEntry[][](1); + workflow.entries[0] = new WorkflowEntry[](1); + // generate a workflow Entry + workflow.entries[0][0] = WorkflowEntry( + 5 ether, + 0, + "cowsay:v0.0.1", + buildWorkflowParamText("hello world"), + new uint256[](0) + ); + uint256 workflowId = tinyHops.storeWorkflow(workflow); + assertEq(workflowId, 1); + assertEq(user1, tinyHops.ownerOf(workflowId)); + tinyHops.startWorkflow{value: 5 ether}(workflowId); + assertEq( + WORKFLOW_STATUS_STARTED, + tinyHops.getWorkFlowStatus(workflowId) + ); + vm.stopPrank(); + vm.startPrank(address(modicumMock)); + uint256[] memory currentJobs = modicumMock.getCurrentJobs(); + assertEq(tinyHops.getStepStatus(workflowId, 0), STEP_STATUS_STARTED); + assertEq(currentJobs.length, 1); + assertEq(modicumMock.getParams(currentJobs[0]), "hello world"); + tinyHops.receiveJobResults(currentJobs[0], "fake cid"); + assertEq(tinyHops.getStepStatus(workflowId, 0), STEP_STATUS_COMPLETED); + ( + uint256[] memory stepIds, + string[] memory cids, + string[] memory params + ) = tinyHops.getWorkFlowResults(workflowId); + assertEq(stepIds[0], 0); + assertEq(cids[0], "fake cid"); + assertEq( + tinyHops.getWorkFlowStatus(workflowId), + WORKFLOW_STATUS_COMPLETED + ); + } + // TODO function testPauseAndResumeJob() public {} function testJobStatus() public {}