Skip to content

Commit

Permalink
Agent communication contract (#15)
Browse files Browse the repository at this point in the history
* first version

* Added struct queue

* Small changes
  • Loading branch information
gabrielfior authored Dec 17, 2024
1 parent dd748a5 commit 031881e
Show file tree
Hide file tree
Showing 3 changed files with 317 additions and 0 deletions.
58 changes: 58 additions & 0 deletions src/NFT/AgentCommunication.sol
Original file line number Diff line number Diff line change
@@ -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;
}
}
94 changes: 94 additions & 0 deletions src/NFT/DoubleEndedStructQueue.sol
Original file line number Diff line number Diff line change
@@ -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;
}
}
165 changes: 165 additions & 0 deletions test/AgentCommunication.t.sol
Original file line number Diff line number Diff line change
@@ -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");
}
}

0 comments on commit 031881e

Please sign in to comment.