From ba18258edfbf77ec83eba749667ce43a8b939c4b Mon Sep 17 00:00:00 2001 From: Gas One Cent <86567384+gas1cent@users.noreply.github.com> Date: Tue, 7 Nov 2023 19:19:43 +0400 Subject: [PATCH] perf: optimize `HttpRequestModule` --- .../modules/request/http_request_module.md | 4 +- .../modules/request/HttpRequestModule.sol | 125 +++-- .../modules/request/IHttpRequestModule.sol | 124 +++-- .../modules/request/HttpRequestModule.t.sol | 498 +++++++----------- 4 files changed, 330 insertions(+), 421 deletions(-) diff --git a/docs/src/content/modules/request/http_request_module.md b/docs/src/content/modules/request/http_request_module.md index 7b4a8ebf..332e16d5 100644 --- a/docs/src/content/modules/request/http_request_module.md +++ b/docs/src/content/modules/request/http_request_module.md @@ -10,8 +10,8 @@ The `HttpRequestModule` is a contract that allows users to request HTTP calls. ### Key Methods -- `decodeRequestData(bytes32 _requestId)`: This method decodes the data for a request given its ID. It returns the URL, HTTP method, body, accounting extension, payment token, and payment amount associated with the request. -- `finalizeRequest(bytes32 _requestId, address)`: This method finalizes a request by paying the proposer if there is a valid response, or releases the requester bond if no valid response was provided. +- `decodeRequestData(bytes calldata _data)`: This method decodes the data for a request given its ID. It returns the URL, HTTP method, body, accounting extension, payment token, and payment amount associated with the request. +- `finalizeRequest`: This method finalizes a request by paying the proposer if there is a valid response, or releases the requester bond if no valid response was provided. ### Request Parameters diff --git a/solidity/contracts/modules/request/HttpRequestModule.sol b/solidity/contracts/modules/request/HttpRequestModule.sol index ef8890fa..ad5324fc 100644 --- a/solidity/contracts/modules/request/HttpRequestModule.sol +++ b/solidity/contracts/modules/request/HttpRequestModule.sol @@ -1,63 +1,62 @@ -// // SPDX-License-Identifier: MIT -// pragma solidity ^0.8.19; - -// // solhint-disable-next-line no-unused-import -// import {Module, IModule} from '@defi-wonderland/prophet-core-contracts/solidity/contracts/Module.sol'; -// import {IOracle} from '@defi-wonderland/prophet-core-contracts/solidity/interfaces/IOracle.sol'; - -// import {IHttpRequestModule} from '../../../interfaces/modules/request/IHttpRequestModule.sol'; - -// contract HttpRequestModule is Module, IHttpRequestModule { -// constructor(IOracle _oracle) Module(_oracle) {} - -// /// @inheritdoc IModule -// function moduleName() public pure returns (string memory _moduleName) { -// _moduleName = 'HttpRequestModule'; -// } - -// /// @inheritdoc IHttpRequestModule -// function decodeRequestData(bytes32 _requestId) public view returns (RequestParameters memory _params) { -// _params = abi.decode(requestData[_requestId], (RequestParameters)); -// } - -// /** -// * @notice Bonds the requester tokens to use as payment for the response proposer. -// */ -// function _afterSetupRequest(bytes32 _requestId, bytes calldata) internal override { -// RequestParameters memory _params = decodeRequestData(_requestId); -// IOracle.Request memory _request = ORACLE.getRequest(_requestId); -// _params.accountingExtension.bond({ -// _bonder: _request.requester, -// _requestId: _requestId, -// _token: _params.paymentToken, -// _amount: _params.paymentAmount -// }); -// } - -// /// @inheritdoc IHttpRequestModule -// function finalizeRequest( -// bytes32 _requestId, -// address _finalizer -// ) external override(IHttpRequestModule, Module) onlyOracle { -// IOracle.Request memory _request = ORACLE.getRequest(_requestId); -// IOracle.Response memory _response = ORACLE.getFinalizedResponse(_requestId); -// RequestParameters memory _params = decodeRequestData(_requestId); -// if (_response.createdAt != 0) { -// _params.accountingExtension.pay({ -// _requestId: _requestId, -// _payer: _request.requester, -// _receiver: _response.proposer, -// _token: _params.paymentToken, -// _amount: _params.paymentAmount -// }); -// } else { -// _params.accountingExtension.release({ -// _bonder: _request.requester, -// _requestId: _requestId, -// _token: _params.paymentToken, -// _amount: _params.paymentAmount -// }); -// } -// emit RequestFinalized(_requestId, _finalizer); -// } -// } +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +// solhint-disable-next-line no-unused-import +import {Module, IModule} from '@defi-wonderland/prophet-core-contracts/solidity/contracts/Module.sol'; +import {IOracle} from '@defi-wonderland/prophet-core-contracts/solidity/interfaces/IOracle.sol'; + +import {IHttpRequestModule} from '../../../interfaces/modules/request/IHttpRequestModule.sol'; + +contract HttpRequestModule is Module, IHttpRequestModule { + constructor(IOracle _oracle) Module(_oracle) {} + + /// @inheritdoc IModule + function moduleName() public pure returns (string memory _moduleName) { + _moduleName = 'HttpRequestModule'; + } + + /// @inheritdoc IHttpRequestModule + function decodeRequestData(bytes calldata _data) public pure returns (RequestParameters memory _params) { + _params = abi.decode(_data, (RequestParameters)); + } + + /// @inheritdoc IHttpRequestModule + function createRequest(bytes32 _requestId, bytes calldata _data, address _requester) external { + RequestParameters memory _params = decodeRequestData(_data); + + _params.accountingExtension.bond({ + _bonder: _requester, + _requestId: _requestId, + _token: _params.paymentToken, + _amount: _params.paymentAmount + }); + } + + /// @inheritdoc IHttpRequestModule + function finalizeRequest( + IOracle.Request calldata _request, + IOracle.Response calldata _response, + address _finalizer + ) external override(IHttpRequestModule, Module) onlyOracle { + RequestParameters memory _params = decodeRequestData(_request.requestModuleData); + + if (ORACLE.createdAt(_getId(_response)) != 0) { + _params.accountingExtension.pay({ + _requestId: _response.requestId, + _payer: _request.requester, + _receiver: _response.proposer, + _token: _params.paymentToken, + _amount: _params.paymentAmount + }); + } else { + _params.accountingExtension.release({ + _bonder: _request.requester, + _requestId: _response.requestId, + _token: _params.paymentToken, + _amount: _params.paymentAmount + }); + } + + emit RequestFinalized(_response.requestId, _response, _finalizer); + } +} diff --git a/solidity/interfaces/modules/request/IHttpRequestModule.sol b/solidity/interfaces/modules/request/IHttpRequestModule.sol index 5c28a4c4..9a01a797 100644 --- a/solidity/interfaces/modules/request/IHttpRequestModule.sol +++ b/solidity/interfaces/modules/request/IHttpRequestModule.sol @@ -1,65 +1,75 @@ -// // SPDX-License-Identifier: MIT -// pragma solidity ^0.8.19; +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; -// import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; -// import {IRequestModule} from -// '@defi-wonderland/prophet-core-contracts/solidity/interfaces/modules/request/IRequestModule.sol'; -// import {IAccountingExtension} from '../../../interfaces/extensions/IAccountingExtension.sol'; +import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; +import {IOracle} from '@defi-wonderland/prophet-core-contracts/solidity/interfaces/IOracle.sol'; +import {IRequestModule} from + '@defi-wonderland/prophet-core-contracts/solidity/interfaces/modules/request/IRequestModule.sol'; +import {IAccountingExtension} from '../../../interfaces/extensions/IAccountingExtension.sol'; -// /* -// * @title HttpRequestModule -// * @notice Module allowing users to request HTTP calls -// */ -// interface IHttpRequestModule is IRequestModule { -// /*/////////////////////////////////////////////////////////////// -// STRUCTS -// //////////////////////////////////////////////////////////////*/ +/* + * @title HttpRequestModule + * @notice Module allowing users to request HTTP calls + */ +interface IHttpRequestModule is IRequestModule { + /*/////////////////////////////////////////////////////////////// + STRUCTS + //////////////////////////////////////////////////////////////*/ -// /** -// * @notice Parameters of the request as stored in the module -// * @param url The url to make the request to -// * @param method The HTTP method to use for the request -// * @param body The HTTP body to use for the request -// * @param accountingExtension The accounting extension used to bond and release tokens -// * @param paymentToken The token used to pay for the request -// * @param paymentAmount The amount of tokens to pay for the request -// */ -// struct RequestParameters { -// string url; -// string body; -// HttpMethod method; -// IAccountingExtension accountingExtension; -// IERC20 paymentToken; -// uint256 paymentAmount; -// } + /** + * @notice Parameters of the request as stored in the module + * @param url The url to make the request to + * @param method The HTTP method to use for the request + * @param body The HTTP body to use for the request + * @param accountingExtension The accounting extension used to bond and release tokens + * @param paymentToken The token used to pay for the request + * @param paymentAmount The amount of tokens to pay for the request + */ + struct RequestParameters { + string url; + string body; + HttpMethod method; + IAccountingExtension accountingExtension; + IERC20 paymentToken; + uint256 paymentAmount; + } -// /*/////////////////////////////////////////////////////////////// -// ENUMS -// //////////////////////////////////////////////////////////////*/ + /*/////////////////////////////////////////////////////////////// + ENUMS + //////////////////////////////////////////////////////////////*/ -// /** -// * @notice Available HTTP methods -// */ -// enum HttpMethod { -// GET, -// POST -// } + /** + * @notice Available HTTP methods + */ + enum HttpMethod { + GET, + POST + } -// /*/////////////////////////////////////////////////////////////// -// LOGIC -// //////////////////////////////////////////////////////////////*/ + /*/////////////////////////////////////////////////////////////// + LOGIC + //////////////////////////////////////////////////////////////*/ -// /** -// * @notice Returns the decoded data for a request -// * @param _requestId The ID of the request -// * @return _params The struct containing the parameters for the request -// */ -// function decodeRequestData(bytes32 _requestId) external view returns (RequestParameters memory _params); + /** + * @notice Returns the decoded data for a request + * @param _data The encoded request parameters + * @return _params The struct containing the parameters for the request + */ + function decodeRequestData(bytes calldata _data) external view returns (RequestParameters memory _params); -// /** -// * @notice Finalizes a request by paying the proposer if there is a valid response -// * or releases the requester bond if no valid response was provided -// * @param _requestId The ID of the request -// */ -// function finalizeRequest(bytes32 _requestId, address) external; -// } + /// @inheritdoc IRequestModule + function createRequest(bytes32 _requestId, bytes calldata _data, address _requester) external; + + /** + * @notice Finalizes the request by paying the proposer for the response or releasing the requester's bond if no response was submitted + * + * @param _request The request that is being finalized + * @param _response The final response + * @param _finalizer The user who triggered the finalization + */ + function finalizeRequest( + IOracle.Request calldata _request, + IOracle.Response calldata _response, + address _finalizer + ) external; +} diff --git a/solidity/test/unit/modules/request/HttpRequestModule.t.sol b/solidity/test/unit/modules/request/HttpRequestModule.t.sol index a053e9fe..e7e300e7 100644 --- a/solidity/test/unit/modules/request/HttpRequestModule.t.sol +++ b/solidity/test/unit/modules/request/HttpRequestModule.t.sol @@ -1,299 +1,199 @@ -// // SPDX-License-Identifier: AGPL-3.0-only -// pragma solidity ^0.8.19; - -// import 'forge-std/Test.sol'; - -// import {Helpers} from '../../../utils/Helpers.sol'; - -// import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; -// import {IOracle} from '@defi-wonderland/prophet-core-contracts/solidity/interfaces/IOracle.sol'; -// import {IModule} from '@defi-wonderland/prophet-core-contracts/solidity/interfaces/IModule.sol'; - -// import {HttpRequestModule, IHttpRequestModule} from '../../../../contracts/modules/request/HttpRequestModule.sol'; - -// import {IAccountingExtension} from '../../../../interfaces/extensions/IAccountingExtension.sol'; - -// /** -// * @dev Harness to set an entry in the requestData mapping, without triggering setup request hooks -// */ - -// contract ForTest_HttpRequestModule is HttpRequestModule { -// constructor(IOracle _oracle) HttpRequestModule(_oracle) {} - -// function forTest_setRequestData(bytes32 _requestId, bytes memory _data) public { -// requestData[_requestId] = _data; -// } -// } - -// /** -// * @title HTTP Request Module Unit tests -// */ -// contract BaseTest is Test, Helpers { -// // Mock request data -// string public constant URL = 'https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=usd'; -// IHttpRequestModule.HttpMethod public constant METHOD = IHttpRequestModule.HttpMethod.GET; -// string public constant BODY = '69420'; - -// // Mock token -// IERC20 public immutable TOKEN = IERC20(makeAddr('ERC20')); -// // The target contract -// ForTest_HttpRequestModule public httpRequestModule; -// // A mock oracle -// IOracle public oracle; -// // A mock accounting extension -// IAccountingExtension public accounting = IAccountingExtension(makeAddr('accounting')); - -// event RequestFinalized(bytes32 indexed _requestId, address _finalizer); - -// /** -// * @notice Deploy the target and mock oracle+accounting extension -// */ -// function setUp() public { -// oracle = IOracle(makeAddr('Oracle')); -// vm.etch(address(oracle), hex'069420'); - -// accounting = IAccountingExtension(makeAddr('AccountingExtension')); -// vm.etch(address(accounting), hex'069420'); - -// httpRequestModule = new ForTest_HttpRequestModule(oracle); -// } -// } - -// contract HttpRequestModule_Unit_ModuleData is BaseTest { -// /** -// * @notice Test that the moduleName function returns the correct name -// */ -// function test_moduleNameReturnsName() public { -// assertEq(httpRequestModule.moduleName(), 'HttpRequestModule'); -// } - -// /** -// * @notice Test that the decodeRequestData function returns the correct values -// */ -// function test_decodeRequestData(bytes32 _requestId, uint256 _amount, IERC20 _token) public { -// bytes memory _requestData = abi.encode( -// IHttpRequestModule.RequestParameters({ -// url: URL, -// method: METHOD, -// body: BODY, -// accountingExtension: accounting, -// paymentToken: _token, -// paymentAmount: _amount -// }) -// ); - -// // Set the request data -// httpRequestModule.forTest_setRequestData(_requestId, _requestData); - -// // Decode the given request data -// IHttpRequestModule.RequestParameters memory _params = httpRequestModule.decodeRequestData(_requestId); - -// // Check: decoded values match original values? -// assertEq(_params.url, URL); -// assertEq(uint256(_params.method), uint256(METHOD)); -// assertEq(_params.body, BODY); -// assertEq(address(_params.accountingExtension), address(accounting)); -// assertEq(address(_params.paymentToken), address(_token)); -// assertEq(_params.paymentAmount, _amount); -// } -// } - -// contract HttpRequestModule_Unit_Setup is BaseTest { -// /** -// * @notice Test that the afterSetupRequest hook: -// * - decodes the request data -// * - gets the request from the oracle -// * - calls the bond function on the accounting extension -// */ -// function test_afterSetupRequestTriggered( -// bytes32 _requestId, -// address _requester, -// uint256 _amount, -// IERC20 _token -// ) public { -// bytes memory _requestData = abi.encode( -// IHttpRequestModule.RequestParameters({ -// url: URL, -// method: METHOD, -// body: BODY, -// accountingExtension: accounting, -// paymentToken: _token, -// paymentAmount: _amount -// }) -// ); - -// IOracle.Request memory _fullRequest; -// _fullRequest.requester = _requester; - -// // Mock and expect IOracle.getRequest to be called -// _mockAndExpect(address(oracle), abi.encodeCall(IOracle.getRequest, (_requestId)), abi.encode(_fullRequest)); - -// // Mock and expect IAccountingExtension.bond to be called -// _mockAndExpect( -// address(accounting), -// abi.encodeWithSignature('bond(address,bytes32,address,uint256)', _requester, _requestId, _token, _amount), -// abi.encode(true) -// ); - -// vm.prank(address(oracle)); -// httpRequestModule.setupRequest(_requestId, _requestData); -// } -// } - -// contract HttpRequestModule_Unit_FinalizeRequest is BaseTest { -// /** -// * @notice Test that finalizeRequest calls: -// * - oracle get request -// * - oracle get response -// * - accounting extension pay -// * - accounting extension release -// */ -// function test_makesCalls( -// bytes32 _requestId, -// address _requester, -// address _proposer, -// uint256 _amount, -// IERC20 _token -// ) public { -// _amount = bound(_amount, 0, type(uint248).max); - -// // Use the correct accounting parameters -// bytes memory _requestData = abi.encode( -// IHttpRequestModule.RequestParameters({ -// url: URL, -// method: METHOD, -// body: BODY, -// accountingExtension: accounting, -// paymentToken: _token, -// paymentAmount: _amount -// }) -// ); - -// IOracle.Request memory _fullRequest; -// _fullRequest.requester = _requester; - -// IOracle.Response memory _fullResponse; -// _fullResponse.proposer = _proposer; -// _fullResponse.createdAt = block.timestamp; - -// // Set the request data -// httpRequestModule.forTest_setRequestData(_requestId, _requestData); - -// // Mock and expect IOracle.getRequest to be called -// _mockAndExpect(address(oracle), abi.encodeCall(IOracle.getRequest, (_requestId)), abi.encode(_fullRequest)); - -// // Mock and expect IOracle.getFinalizedResponse to be called -// _mockAndExpect( -// address(oracle), abi.encodeCall(IOracle.getFinalizedResponse, (_requestId)), abi.encode(_fullResponse) -// ); - -// // Mock and expect IAccountingExtension.pay to be called -// _mockAndExpect( -// address(accounting), -// abi.encodeCall(IAccountingExtension.pay, (_requestId, _requester, _proposer, _token, _amount)), -// abi.encode() -// ); - -// vm.startPrank(address(oracle)); -// httpRequestModule.finalizeRequest(_requestId, address(oracle)); - -// // Test the release flow -// _fullResponse.createdAt = 0; - -// // Update mock call to return the response with createdAt = 0 -// _mockAndExpect( -// address(oracle), abi.encodeCall(IOracle.getFinalizedResponse, (_requestId)), abi.encode(_fullResponse) -// ); - -// // Mock and expect IAccountingExtension.release to be called -// _mockAndExpect( -// address(accounting), -// abi.encodeCall(IAccountingExtension.release, (_requester, _requestId, _token, _amount)), -// abi.encode(true) -// ); - -// httpRequestModule.finalizeRequest(_requestId, address(this)); -// } - -// function test_emitsEvent( -// bytes32 _requestId, -// address _requester, -// address _proposer, -// uint256 _amount, -// IERC20 _token -// ) public { -// // Use the correct accounting parameters -// bytes memory _requestData = abi.encode( -// IHttpRequestModule.RequestParameters({ -// url: URL, -// method: METHOD, -// body: BODY, -// accountingExtension: accounting, -// paymentToken: _token, -// paymentAmount: _amount -// }) -// ); - -// IOracle.Request memory _fullRequest; -// _fullRequest.requester = _requester; - -// IOracle.Response memory _fullResponse; -// _fullResponse.proposer = _proposer; -// _fullResponse.createdAt = block.timestamp; - -// // Set the request data -// httpRequestModule.forTest_setRequestData(_requestId, _requestData); - -// // Mock and expect IOracle.getRequest to be called -// _mockAndExpect(address(oracle), abi.encodeCall(IOracle.getRequest, (_requestId)), abi.encode(_fullRequest)); - -// // Mock and expect IOracle.getFinalizedResponse to be called -// _mockAndExpect( -// address(oracle), abi.encodeCall(IOracle.getFinalizedResponse, (_requestId)), abi.encode(_fullResponse) -// ); - -// // Mock and expect IAccountingExtension.pay to be called -// _mockAndExpect( -// address(accounting), -// abi.encodeCall(IAccountingExtension.pay, (_requestId, _requester, _proposer, _token, _amount)), -// abi.encode() -// ); - -// vm.startPrank(address(oracle)); -// httpRequestModule.finalizeRequest(_requestId, address(oracle)); - -// // Test the release flow -// _fullResponse.createdAt = 0; - -// // Update mock call to return the response with createdAt = 0 -// _mockAndExpect( -// address(oracle), abi.encodeCall(IOracle.getFinalizedResponse, (_requestId)), abi.encode(_fullResponse) -// ); - -// // Mock and expect IAccountingExtension.release to be called -// _mockAndExpect( -// address(accounting), -// abi.encodeCall(IAccountingExtension.release, (_requester, _requestId, _token, _amount)), -// abi.encode(true) -// ); - -// // Check: is the event emitted? -// vm.expectEmit(true, true, true, true, address(httpRequestModule)); -// emit RequestFinalized(_requestId, address(this)); - -// httpRequestModule.finalizeRequest(_requestId, address(this)); -// } - -// /** -// * @notice Test that the finalizeRequest reverts if caller is not the oracle -// */ -// function test_revertsIfWrongCaller(bytes32 _requestId, address _caller) public { -// vm.assume(_caller != address(oracle)); - -// // Check: does it revert if not called by the Oracle? -// vm.expectRevert(abi.encodeWithSelector(IModule.Module_OnlyOracle.selector)); - -// vm.prank(_caller); -// httpRequestModule.finalizeRequest(_requestId, address(_caller)); -// } -// } +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.19; + +import 'forge-std/Test.sol'; + +import {Helpers} from '../../../utils/Helpers.sol'; + +import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; +import {IOracle} from '@defi-wonderland/prophet-core-contracts/solidity/interfaces/IOracle.sol'; +import {IModule} from '@defi-wonderland/prophet-core-contracts/solidity/interfaces/IModule.sol'; + +import {HttpRequestModule, IHttpRequestModule} from '../../../../contracts/modules/request/HttpRequestModule.sol'; + +import {IAccountingExtension} from '../../../../interfaces/extensions/IAccountingExtension.sol'; + +/** + * @title HTTP Request Module Unit tests + */ +contract BaseTest is Test, Helpers { + // Mock request data + string public constant URL = 'https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=usd'; + IHttpRequestModule.HttpMethod public constant METHOD = IHttpRequestModule.HttpMethod.GET; + string public constant BODY = '69420'; + + // Mock token + IERC20 public immutable TOKEN = IERC20(makeAddr('ERC20')); + // The target contract + HttpRequestModule public httpRequestModule; + // A mock oracle + IOracle public oracle; + // A mock accounting extension + IAccountingExtension public accounting = IAccountingExtension(makeAddr('accounting')); + IOracle.Response public mockResponse; + address internal _proposer = makeAddr('proposer'); + bytes32 public mockId = bytes32('69'); + + event RequestFinalized(bytes32 indexed _requestId, address _finalizer); + + /** + * @notice Deploy the target and mock oracle+accounting extension + */ + function setUp() public { + oracle = IOracle(makeAddr('Oracle')); + vm.etch(address(oracle), hex'069420'); + + accounting = IAccountingExtension(makeAddr('AccountingExtension')); + vm.etch(address(accounting), hex'069420'); + + httpRequestModule = new HttpRequestModule(oracle); + mockResponse = IOracle.Response({proposer: _proposer, requestId: mockId, response: bytes('')}); + } +} + +contract HttpRequestModule_Unit_ModuleData is BaseTest { + /** + * @notice Test that the moduleName function returns the correct name + */ + function test_moduleNameReturnsName() public { + assertEq(httpRequestModule.moduleName(), 'HttpRequestModule'); + } + + /** + * @notice Test that the decodeRequestData function returns the correct values + */ + function test_decodeRequestData(bytes32 _requestId, uint256 _amount, IERC20 _token) public { + bytes memory _requestData = abi.encode( + IHttpRequestModule.RequestParameters({ + url: URL, + method: METHOD, + body: BODY, + accountingExtension: accounting, + paymentToken: _token, + paymentAmount: _amount + }) + ); + + // Decode the given request data + IHttpRequestModule.RequestParameters memory _params = httpRequestModule.decodeRequestData(_requestData); + + // Check: decoded values match original values? + assertEq(_params.url, URL); + assertEq(uint256(_params.method), uint256(METHOD)); + assertEq(_params.body, BODY); + assertEq(address(_params.accountingExtension), address(accounting)); + assertEq(address(_params.paymentToken), address(_token)); + assertEq(_params.paymentAmount, _amount); + } +} + +contract HttpRequestModule_Unit_FinalizeRequest is BaseTest { + /** + * @notice Test that finalizeRequest calls: + * - oracle get request + * - oracle get response + * - accounting extension pay + * - accounting extension release + */ + function test_makesCalls( + bytes32 _requestId, + address _requester, + address _proposer, + uint256 _amount, + IERC20 _token, + IOracle.Request calldata _request + ) public { + _amount = bound(_amount, 0, type(uint248).max); + + // Use the correct accounting parameters + bytes memory _requestData = abi.encode( + IHttpRequestModule.RequestParameters({ + url: URL, + method: METHOD, + body: BODY, + accountingExtension: accounting, + paymentToken: _token, + paymentAmount: _amount + }) + ); + + // Mock and expect IAccountingExtension.pay to be called + _mockAndExpect( + address(accounting), + abi.encodeCall(IAccountingExtension.pay, (_requestId, _requester, _proposer, _token, _amount)), + abi.encode() + ); + + vm.startPrank(address(oracle)); + httpRequestModule.finalizeRequest(_request, mockResponse, address(oracle)); + + // Mock and expect IAccountingExtension.release to be called + _mockAndExpect( + address(accounting), + abi.encodeCall(IAccountingExtension.release, (_requester, _requestId, _token, _amount)), + abi.encode(true) + ); + + httpRequestModule.finalizeRequest(_request, mockResponse, address(this)); + } + + function test_emitsEvent( + bytes32 _requestId, + address _requester, + address _proposer, + uint256 _amount, + IERC20 _token, + IOracle.Request calldata _request + ) public { + // Use the correct accounting parameters + bytes memory _requestData = abi.encode( + IHttpRequestModule.RequestParameters({ + url: URL, + method: METHOD, + body: BODY, + accountingExtension: accounting, + paymentToken: _token, + paymentAmount: _amount + }) + ); + + // Mock and expect IAccountingExtension.pay to be called + _mockAndExpect( + address(accounting), + abi.encodeCall(IAccountingExtension.pay, (_requestId, _requester, _proposer, _token, _amount)), + abi.encode() + ); + + vm.startPrank(address(oracle)); + httpRequestModule.finalizeRequest(_request, mockResponse, address(oracle)); + + // Update mock call to return the response's createdAt + _mockAndExpect(address(oracle), abi.encodeCall(IOracle.createdAt, (_getId(mockResponse))), abi.encode(0)); + + // Mock and expect IAccountingExtension.release to be called + _mockAndExpect( + address(accounting), + abi.encodeCall(IAccountingExtension.release, (_requester, _requestId, _token, _amount)), + abi.encode(true) + ); + + // Check: is the event emitted? + vm.expectEmit(true, true, true, true, address(httpRequestModule)); + emit RequestFinalized(_requestId, address(this)); + + httpRequestModule.finalizeRequest(_request, mockResponse, address(this)); + } + + /** + * @notice Test that the finalizeRequest reverts if caller is not the oracle + */ + function test_revertsIfWrongCaller(bytes32 _requestId, address _caller, IOracle.Request calldata _request) public { + vm.assume(_caller != address(oracle)); + + // Check: does it revert if not called by the Oracle? + vm.expectRevert(abi.encodeWithSelector(IModule.Module_OnlyOracle.selector)); + + vm.prank(_caller); + httpRequestModule.finalizeRequest(_request, mockResponse, address(_caller)); + } +}