diff --git a/abis/PoRepMarket.json b/abis/PoRepMarket.json index 7f3f848..62a43f2 100644 --- a/abis/PoRepMarket.json +++ b/abis/PoRepMarket.json @@ -69,6 +69,78 @@ "outputs": [], "stateMutability": "nonpayable" }, + { + "type": "function", + "name": "getCompletedDeals", + "inputs": [], + "outputs": [ + { + "name": "completedDeals", + "type": "tuple[]", + "internalType": "struct PoRepMarket.DealProposal[]", + "components": [ + { + "name": "dealId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "client", + "type": "address", + "internalType": "address" + }, + { + "name": "provider", + "type": "uint64", + "internalType": "CommonTypes.FilActorId" + }, + { + "name": "requirements", + "type": "tuple", + "internalType": "struct SLITypes.SLIThresholds", + "components": [ + { + "name": "retrievabilityPct", + "type": "uint8", + "internalType": "uint8" + }, + { + "name": "bandwidthMbps", + "type": "uint16", + "internalType": "uint16" + }, + { + "name": "latencyMs", + "type": "uint16", + "internalType": "uint16" + }, + { + "name": "indexingPct", + "type": "uint8", + "internalType": "uint8" + } + ] + }, + { + "name": "validator", + "type": "address", + "internalType": "address" + }, + { + "name": "state", + "type": "uint8", + "internalType": "enum PoRepMarket.DealState" + }, + { + "name": "railId", + "type": "uint256", + "internalType": "uint256" + } + ] + } + ], + "stateMutability": "view" + }, { "type": "function", "name": "getDealProposal", diff --git a/abis/SLITypes.json b/abis/SLITypes.json deleted file mode 100644 index fe51488..0000000 --- a/abis/SLITypes.json +++ /dev/null @@ -1 +0,0 @@ -[] diff --git a/gen-abis.sh b/gen-abis.sh index 9340af3..4b0175f 100755 --- a/gen-abis.sh +++ b/gen-abis.sh @@ -5,7 +5,7 @@ set -euo pipefail rm -rf abis mkdir abis -for i in $(find src -name '*.sol' ! -path "*/interfaces/*"); do +for i in $(find src -name '*.sol' ! -path "*/interfaces/*" ! -path "*/types/*"); do echo "Generating ABI file for $i" i="$(basename "$i")" name="${i%.sol}" diff --git a/src/PoRepMarket.sol b/src/PoRepMarket.sol index d8b0b32..3438c0e 100644 --- a/src/PoRepMarket.sol +++ b/src/PoRepMarket.sol @@ -10,6 +10,7 @@ import {ISPRegistry} from "./interfaces/ISPRegistry.sol"; import {ValidatorFactory} from "./ValidatorFactory.sol"; import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import {SLITypes} from "./types/SLITypes.sol"; +import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; /** * @title PoRepMarket contract @@ -17,6 +18,7 @@ import {SLITypes} from "./types/SLITypes.sol"; * @notice PoRepMarket contract */ contract PoRepMarket is Initializable, AccessControlUpgradeable, UUPSUpgradeable { + using EnumerableSet for EnumerableSet.UintSet; /** * @notice role to manage contract upgrades */ @@ -25,6 +27,7 @@ contract PoRepMarket is Initializable, AccessControlUpgradeable, UUPSUpgradeable /// @custom:storage-location erc7201:porepmarket.storage.DealProposalsStorage struct DealProposalsStorage { mapping(uint256 dealId => DealProposal) _dealProposals; + EnumerableSet.UintSet _dealIdsReadyForPayment; ISPRegistry _SPRegistryContract; ValidatorFactory _validatorFactoryContract; address _clientSmartContract; @@ -42,6 +45,15 @@ contract PoRepMarket is Initializable, AccessControlUpgradeable, UUPSUpgradeable } } + /** + * @dev Returns the storage struct for the PoRepMarket contract. + * @notice function to allow acess to storage + * @return DealProposalsStorage storage struct + */ + function s() private pure returns (DealProposalsStorage storage) { + return _getDealProposalsStorage(); + } + /** * @notice DealState enum * @dev DealState enum is an enum that contains the states of a deal @@ -152,7 +164,7 @@ contract PoRepMarket is Initializable, AccessControlUpgradeable, UUPSUpgradeable _grantRole(DEFAULT_ADMIN_ROLE, _admin); _grantRole(UPGRADER_ROLE, _admin); - DealProposalsStorage storage $ = _getDealProposalsStorage(); + DealProposalsStorage storage $ = s(); $._validatorFactoryContract = ValidatorFactory(_validatorFactory); $._SPRegistryContract = ISPRegistry(_spRegistry); $._clientSmartContract = _clientSmartContract; @@ -172,7 +184,7 @@ contract PoRepMarket is Initializable, AccessControlUpgradeable, UUPSUpgradeable revert InvalidIndexingPct(requirements.indexingPct); } - DealProposalsStorage storage $ = _getDealProposalsStorage(); + DealProposalsStorage storage $ = s(); CommonTypes.FilActorId provider = $._SPRegistryContract.getProviderForDeal(requirements, terms); if (CommonTypes.FilActorId.unwrap(provider) == 0) { @@ -202,7 +214,7 @@ contract PoRepMarket is Initializable, AccessControlUpgradeable, UUPSUpgradeable * @param railId The id of the rail */ function updateValidatorAndRailId(uint256 dealId, uint256 railId) external { - DealProposalsStorage storage $ = _getDealProposalsStorage(); + DealProposalsStorage storage $ = s(); DealProposal storage dp = $._dealProposals[dealId]; _ensureDealExists(dp); @@ -228,7 +240,7 @@ contract PoRepMarket is Initializable, AccessControlUpgradeable, UUPSUpgradeable * @return DealProposal The deal proposal */ function getDealProposal(uint256 dealId) external view returns (DealProposal memory) { - DealProposalsStorage storage $ = _getDealProposalsStorage(); + DealProposalsStorage storage $ = s(); return $._dealProposals[dealId]; } @@ -238,7 +250,7 @@ contract PoRepMarket is Initializable, AccessControlUpgradeable, UUPSUpgradeable * @param dealId The id of the deal proposal */ function acceptDeal(uint256 dealId) external { - DealProposalsStorage storage $ = _getDealProposalsStorage(); + DealProposalsStorage storage $ = s(); DealProposal storage dp = $._dealProposals[dealId]; _ensureDealExists(dp); @@ -258,7 +270,7 @@ contract PoRepMarket is Initializable, AccessControlUpgradeable, UUPSUpgradeable * @param dealId The id of the deal proposal */ function completeDeal(uint256 dealId) external { - DealProposalsStorage storage $ = _getDealProposalsStorage(); + DealProposalsStorage storage $ = s(); DealProposal storage dp = $._dealProposals[dealId]; _ensureDealExists(dp); @@ -267,9 +279,11 @@ contract PoRepMarket is Initializable, AccessControlUpgradeable, UUPSUpgradeable if (msg.sender != $._clientSmartContract) revert NotTheClientSmartContract(dealId, msg.sender); dp.state = DealState.Completed; + $._dealIdsReadyForPayment.add(dealId); // TODO: Call $._SPRegistryContract.commitCapacity(dp.provider, actualSizeBytes) // when completeDeal signature is updated with actualSizeBytes from Client contract. // REF: Client.sol (PR #4) + emit DealCompleted(dealId, msg.sender, dp.provider); } @@ -279,7 +293,7 @@ contract PoRepMarket is Initializable, AccessControlUpgradeable, UUPSUpgradeable * @param dealId The id of the deal proposal */ function rejectDeal(uint256 dealId) external { - DealProposalsStorage storage $ = _getDealProposalsStorage(); + DealProposalsStorage storage $ = s(); DealProposal storage dp = $._dealProposals[dealId]; _ensureDealExists(dp); @@ -293,6 +307,31 @@ contract PoRepMarket is Initializable, AccessControlUpgradeable, UUPSUpgradeable emit DealRejected(dealId, msg.sender); } + /** + * @notice Gets all completed deals + * @dev Iterates through all deals and returns only those with Completed state + * @return completedDeals Array of completed deal proposals + */ + function getCompletedDeals() external view returns (DealProposal[] memory completedDeals) { + DealProposalsStorage storage $ = s(); + uint256[] memory completedDealsIds = $._dealIdsReadyForPayment.values(); + completedDeals = new DealProposal[](completedDealsIds.length); + uint256 dealCounter = 0; + + for (uint256 i = 0; i < completedDealsIds.length; i++) { + DealProposal memory dp = $._dealProposals[completedDealsIds[i]]; + if (dp.state == DealState.Completed) { + completedDeals[dealCounter] = dp; + dealCounter++; + } + } + + // solhint-disable-next-line no-inline-assembly + assembly ("memory-safe") { + mstore(completedDeals, dealCounter) + } + } + /** * @notice Ensures a deal exists * @dev Ensures a deal exists by checking if the deal id exists diff --git a/test/PoRepMarket.t.sol b/test/PoRepMarket.t.sol index 116196a..ab78775 100644 --- a/test/PoRepMarket.t.sol +++ b/test/PoRepMarket.t.sol @@ -10,6 +10,7 @@ import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.s import {CommonTypes} from "filecoin-solidity/v0.8/types/CommonTypes.sol"; import {IAccessControl} from "@openzeppelin/contracts/access/IAccessControl.sol"; import {SLITypes} from "../src/types/SLITypes.sol"; +import {PoRepMarketContractMock} from "./contracts/PoRepMarketContractMock.sol"; // solhint-disable-next-line max-states-count contract PoRepMarketTest is Test { @@ -62,6 +63,22 @@ contract PoRepMarketTest is Test { validatorFactory.setValidator(validatorAddress, true); } + function createDealProposal(uint256 proposalDealId, PoRepMarket.DealState state) + public + view + returns (PoRepMarket.DealProposal memory) + { + return PoRepMarket.DealProposal({ + dealId: proposalDealId, + client: clientAddress, + provider: providerFilActorId, + requirements: defaultRequirements, + validator: validatorAddress, + railId: railId, + state: state + }); + } + function testProposeDealEmitsEvent() public { vm.prank(clientAddress); vm.expectEmit(true, true, true, true); @@ -232,6 +249,30 @@ contract PoRepMarketTest is Test { poRepMarket.completeDeal(dealId); } + function testShouldAddDealIdToCompletedDealsIdsSet() public { + PoRepMarketContractMock impl = new PoRepMarketContractMock(); + bytes memory initData = abi.encodeWithSignature( + "initialize(address,address,address,address)", + adminAddress, + address(validatorFactory), + address(spRegistry), + clientSmartContractAddress + ); + ERC1967Proxy proxy = new ERC1967Proxy(address(impl), initData); + PoRepMarketContractMock porepMarekMock = PoRepMarketContractMock(address(proxy)); + vm.prank(clientAddress); + porepMarekMock.proposeDeal(defaultRequirements, defaultTerms); + vm.prank(providerOwnerAddress); + porepMarekMock.acceptDeal(dealId); + + vm.prank(clientSmartContractAddress); + porepMarekMock.completeDeal(dealId); + + uint256[] memory completedDealsIds = porepMarekMock.getCompletedDealsIds(); + assertEq(completedDealsIds.length, 1); + assertEq(completedDealsIds[0], dealId); + } + function testCompleteDealRevertsWhenDealDoesNotExist() public { vm.expectRevert(abi.encodeWithSelector(PoRepMarket.DealDoesNotExist.selector)); poRepMarket.completeDeal(dealId); @@ -352,4 +393,27 @@ contract PoRepMarketTest is Test { vm.expectRevert(abi.encodeWithSelector(PoRepMarket.InvalidIndexingPct.selector, uint8(101))); poRepMarket.proposeDeal(badRequirements, defaultTerms); } + + function testGetCompletedDeals() public { + PoRepMarketContractMock porepMarekMock = new PoRepMarketContractMock(); + uint256[] memory ids = new uint256[](5); + ids[0] = 1; + ids[1] = 2; + ids[2] = 3; + ids[3] = 4; + ids[4] = 5; + porepMarekMock.setDealProposal(createDealProposal(ids[0], PoRepMarket.DealState.Completed)); + porepMarekMock.setDealProposal(createDealProposal(ids[1], PoRepMarket.DealState.Accepted)); + porepMarekMock.setDealProposal(createDealProposal(ids[2], PoRepMarket.DealState.Proposed)); + porepMarekMock.setDealProposal(createDealProposal(ids[3], PoRepMarket.DealState.Completed)); + porepMarekMock.setDealProposal(createDealProposal(ids[4], PoRepMarket.DealState.Rejected)); + porepMarekMock.setDealIdsReadyForPayment(ids); + + PoRepMarket.DealProposal[] memory dealProposal = porepMarekMock.getCompletedDeals(); + assertEq(dealProposal.length, 2); + assertEq(dealProposal[0].dealId, ids[0]); + assertTrue(dealProposal[0].state == PoRepMarket.DealState.Completed); + assertEq(dealProposal[1].dealId, ids[3]); + assertTrue(dealProposal[1].state == PoRepMarket.DealState.Completed); + } } diff --git a/test/contracts/PoRepMarketContractMock.sol b/test/contracts/PoRepMarketContractMock.sol new file mode 100644 index 0000000..5364086 --- /dev/null +++ b/test/contracts/PoRepMarketContractMock.sol @@ -0,0 +1,33 @@ +// solhint-disable use-natspec +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.25; + +import {PoRepMarket} from "../../src/PoRepMarket.sol"; +import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; + +contract PoRepMarketContractMock is PoRepMarket { + using EnumerableSet for EnumerableSet.UintSet; + + function _getStorage() private pure returns (PoRepMarket.DealProposalsStorage storage $) { + // solhint-disable-next-line no-inline-assembly + assembly ("memory-safe") { + $.slot := 0xea093611145db18b250f1cd58e07fc50de512902beb662a10f8e6d1dd55f6700 + } + } + + function setDealProposal(PoRepMarket.DealProposal calldata dealProposal) external { + _getStorage()._dealProposals[++_getStorage()._dealIdCounter] = dealProposal; + } + + function setDealIdsReadyForPayment(uint256[] calldata dealIds) external { + for (uint256 i = 0; i < dealIds.length; i++) { + _getStorage()._dealIdsReadyForPayment.add(dealIds[i]); + } + } + + function getCompletedDealsIds() public view returns (uint256[] memory) { + return _getStorage()._dealIdsReadyForPayment.values(); + } +} +