diff --git a/src/NFT/AgentCommunication.sol b/src/NFT/AgentCommunication.sol new file mode 100644 index 0000000..6783c31 --- /dev/null +++ b/src/NFT/AgentCommunication.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.22; + +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import "./DoubleEndedStructQueue.sol"; + +contract AgentCommunication is Ownable { + error MessageNotSentByAgent(); + + mapping(address => DoubleEndedStructQueue.Bytes32Deque) public queues; + uint256 public minimumValueForSendingMessageInWei; + + event NewMessageSent(address indexed sender, address indexed agentAddress, bytes32 message); + event MessagePopped(address indexed agentAddress, bytes32 message); + + constructor() Ownable(msg.sender) { + minimumValueForSendingMessageInWei = 10000000000000; // 0.00001 xDAI + } + + modifier mustPayMoreThanMinimum() { + require(msg.value >= minimumValueForSendingMessageInWei, "Insufficient message value"); + _; + } + + function adjustMinimumValueForSendingMessage(uint256 newValue) public onlyOwner { + minimumValueForSendingMessageInWei = newValue; + } + + function countMessages(address agentAddress) public view returns (uint256) { + return DoubleEndedStructQueue.length(queues[agentAddress]); + } + + function sendMessage(address agentAddress, DoubleEndedStructQueue.MessageContainer memory message) + public + payable + mustPayMoreThanMinimum + { + DoubleEndedStructQueue.pushBack(queues[agentAddress], message); + emit NewMessageSent(msg.sender, agentAddress, message.message); + } + + function getAtIndex(address agentAddress, uint256 idx) + public + view + returns (DoubleEndedStructQueue.MessageContainer memory) + { + return DoubleEndedStructQueue.at(queues[agentAddress], idx); + } + + function popNextMessage(address agentAddress) public returns (DoubleEndedStructQueue.MessageContainer memory) { + if (msg.sender != agentAddress) { + revert MessageNotSentByAgent(); + } + DoubleEndedStructQueue.MessageContainer memory message = DoubleEndedStructQueue.popFront(queues[agentAddress]); + emit MessagePopped(agentAddress, message.message); + return message; + } +} diff --git a/src/NFT/DoubleEndedStructQueue.sol b/src/NFT/DoubleEndedStructQueue.sol new file mode 100644 index 0000000..b7975ca --- /dev/null +++ b/src/NFT/DoubleEndedStructQueue.sol @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {Panic} from "@openzeppelin/contracts/utils/Panic.sol"; + +// Based on OpenZeppelin's DoubleEndedQueue, but with a custom struct as data type instead of bytes32. +// See https://docs.openzeppelin.com/contracts/5.x/api/utils#DoubleEndedQueue +library DoubleEndedStructQueue { + struct MessageContainer { + address sender; + address recipient; + bytes32 message; + } + + struct Bytes32Deque { + uint128 _begin; + uint128 _end; + mapping(uint128 index => MessageContainer) _data; + } + + function pushBack(Bytes32Deque storage deque, MessageContainer memory container) internal { + unchecked { + uint128 backIndex = deque._end; + if (backIndex + 1 == deque._begin) Panic.panic(Panic.RESOURCE_ERROR); + deque._data[backIndex] = container; + deque._end = backIndex + 1; + } + } + + function popBack(Bytes32Deque storage deque) internal returns (MessageContainer memory value) { + unchecked { + uint128 backIndex = deque._end; + if (backIndex == deque._begin) Panic.panic(Panic.EMPTY_ARRAY_POP); + --backIndex; + value = deque._data[backIndex]; + delete deque._data[backIndex]; + deque._end = backIndex; + } + } + + function pushFront(Bytes32Deque storage deque, MessageContainer memory value) internal { + unchecked { + uint128 frontIndex = deque._begin - 1; + if (frontIndex == deque._end) Panic.panic(Panic.RESOURCE_ERROR); + deque._data[frontIndex] = value; + deque._begin = frontIndex; + } + } + + function popFront(Bytes32Deque storage deque) internal returns (MessageContainer memory value) { + unchecked { + uint128 frontIndex = deque._begin; + if (frontIndex == deque._end) Panic.panic(Panic.EMPTY_ARRAY_POP); + value = deque._data[frontIndex]; + delete deque._data[frontIndex]; + deque._begin = frontIndex + 1; + } + } + + function front(Bytes32Deque storage deque) internal view returns (MessageContainer memory value) { + if (empty(deque)) Panic.panic(Panic.ARRAY_OUT_OF_BOUNDS); + return deque._data[deque._begin]; + } + + function back(Bytes32Deque storage deque) internal view returns (MessageContainer memory value) { + if (empty(deque)) Panic.panic(Panic.ARRAY_OUT_OF_BOUNDS); + unchecked { + return deque._data[deque._end - 1]; + } + } + + function at(Bytes32Deque storage deque, uint256 index) internal view returns (MessageContainer memory value) { + if (index >= length(deque)) Panic.panic(Panic.ARRAY_OUT_OF_BOUNDS); + // By construction, length is a uint128, so the check above ensures that index can be safely downcast to uint128 + unchecked { + return deque._data[deque._begin + uint128(index)]; + } + } + + function clear(Bytes32Deque storage deque) internal { + deque._begin = 0; + deque._end = 0; + } + + function length(Bytes32Deque storage deque) internal view returns (uint256) { + unchecked { + return uint256(deque._end - deque._begin); + } + } + + function empty(Bytes32Deque storage deque) internal view returns (bool) { + return deque._end == deque._begin; + } +} diff --git a/test/AgentCommunication.t.sol b/test/AgentCommunication.t.sol new file mode 100644 index 0000000..5ed52c7 --- /dev/null +++ b/test/AgentCommunication.t.sol @@ -0,0 +1,165 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.22; + +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import "forge-std/Test.sol"; +import {AgentCommunication} from "../src/NFT/AgentCommunication.sol"; +import "../src/NFT/DoubleEndedStructQueue.sol"; + +contract AgentCommunicationTest is Test { + AgentCommunication agentComm; + address owner = address(0x123); + address agent = address(0x456); + + function setUp() public { + vm.startPrank(owner); + agentComm = new AgentCommunication(); + vm.stopPrank(); + } + + function testInitialMinimumValue() public { + uint256 expectedValue = 10000000000000; // 0.00001 xDAI + assertEq(agentComm.minimumValueForSendingMessageInWei(), expectedValue); + } + + function testAdjustMinimumValue() public { + uint256 newValue = 20000000000000; // 0.00002 xDAI + vm.startPrank(owner); + agentComm.adjustMinimumValueForSendingMessage(newValue); + vm.stopPrank(); + assertEq(agentComm.minimumValueForSendingMessageInWei(), newValue); + } + + function testOnlyOwnerCanAdjustMinimumValue() public { + uint256 newValue = 20000000000000; // 0.00002 xDAI + address nonOwner = address(0x789); + + // Attempt to adjust the minimum value from a non-owner address + vm.startPrank(nonOwner); + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, address(nonOwner))); + agentComm.adjustMinimumValueForSendingMessage(newValue); + vm.stopPrank(); + + // Verify that the value has not changed + assertEq(agentComm.minimumValueForSendingMessageInWei(), 10000000000000); + } + + function testSendMessage() public { + DoubleEndedStructQueue.MessageContainer memory message = DoubleEndedStructQueue.MessageContainer({ + sender: agent, // or any appropriate address + recipient: address(0x789), // or any appropriate address + message: "Hello, Agent!" // Ensure this is a bytes32 type + }); + vm.deal(agent, 1 ether); + vm.startPrank(agent); + agentComm.sendMessage{value: 10000000000000}(agent, message); + vm.stopPrank(); + + DoubleEndedStructQueue.MessageContainer memory storedMessage = agentComm.getAtIndex(agent, 0); + assertEq(storedMessage.message, message.message); + } + + function testSendMessageInsufficientValue() public { + DoubleEndedStructQueue.MessageContainer memory message = DoubleEndedStructQueue.MessageContainer({ + sender: agent, + recipient: address(0x789), // or any appropriate address + message: bytes32("Hello, Agent!") // Ensure this is a bytes32 type + }); + vm.deal(agent, 1 ether); + vm.startPrank(agent); + vm.expectRevert("Insufficient message value"); + agentComm.sendMessage{value: 5000}(agent, message); + vm.stopPrank(); + } + + function testNewMessageSentEvent() public { + address recipient = address(0x789); + DoubleEndedStructQueue.MessageContainer memory message = DoubleEndedStructQueue.MessageContainer({ + sender: agent, + recipient: recipient, + message: bytes32("Hello, Agent!") // Ensure this is a bytes32 type + }); + vm.deal(agent, 1 ether); + vm.startPrank(agent); + + // Expect the NewMessageSent event to be emitted + vm.expectEmit(true, true, false, true); + emit AgentCommunication.NewMessageSent(agent, message.recipient, message.message); + + // Send the message + agentComm.sendMessage{value: 0.2 ether}(recipient, message); + vm.stopPrank(); + } + + function testPopNextMessage() public { + DoubleEndedStructQueue.MessageContainer memory message = DoubleEndedStructQueue.MessageContainer({ + sender: agent, + recipient: address(0x789), // or any appropriate address + message: bytes32("Hello, Agent!") // Ensure this is a bytes32 type + }); + vm.deal(agent, 1 ether); + vm.startPrank(agent); + agentComm.sendMessage{value: 10000000000000}(agent, message); + vm.stopPrank(); + + // Expect the MessagePopped event to be emitted + vm.expectEmit(true, true, false, true); + emit AgentCommunication.MessagePopped(agent, message.message); + vm.startPrank(agent); + DoubleEndedStructQueue.MessageContainer memory poppedMessage = agentComm.popNextMessage(agent); + vm.stopPrank(); + + assertEq(poppedMessage.message, message.message); + uint256 numMessages = agentComm.countMessages(agent); + assertEq(numMessages, 0); + } + + // ToDo - reset name + function testPopNextMessageNotByAgent() public { + DoubleEndedStructQueue.MessageContainer memory message = DoubleEndedStructQueue.MessageContainer({ + sender: agent, + recipient: address(0x789), // or any appropriate address + message: bytes32("Hello, Agent!") // Ensure this is a bytes32 type + }); + vm.deal(agent, 1 ether); + vm.startPrank(agent); + agentComm.sendMessage{value: 10000000000000}(agent, message); + vm.stopPrank(); + + address notAgent = address(0x789); + vm.startPrank(notAgent); + vm.expectRevert(AgentCommunication.MessageNotSentByAgent.selector); + agentComm.popNextMessage(agent); + vm.stopPrank(); + } + + function testCountMessages() public { + // Initialize a message + DoubleEndedStructQueue.MessageContainer memory message1 = DoubleEndedStructQueue.MessageContainer({ + sender: agent, + recipient: address(0x789), + message: bytes32("Hello, Agent 1!") // Ensure this is a bytes32 type + }); + + DoubleEndedStructQueue.MessageContainer memory message2 = DoubleEndedStructQueue.MessageContainer({ + sender: agent, + recipient: address(0x789), + message: bytes32("Hello, Agent 2!") // Ensure this is a bytes32 type + }); + + // Fund the agent and start the prank + vm.deal(agent, 1 ether); + vm.startPrank(agent); + + // Send two messages + agentComm.sendMessage{value: 10000000000000}(agent, message1); + agentComm.sendMessage{value: 10000000000000}(agent, message2); + + // Stop the prank + vm.stopPrank(); + + // Check the count of messages + uint256 messageCount = agentComm.countMessages(agent); + assertEq(messageCount, 2, "The message count should be 2"); + } +}