Skip to content

Commit

Permalink
add Mailbox contract (#5)
Browse files Browse the repository at this point in the history
* base Mailbox implementation

* change underlying data struct to enable needed message traversal and reading methods

* impl a func to mark a message as read this way freeing a slot for a new message

* allow a recipient to request a message without specifying a sender
  • Loading branch information
anton-ptashnik authored Oct 7, 2024
1 parent 8389dc4 commit 5460c6c
Show file tree
Hide file tree
Showing 6 changed files with 464 additions and 1 deletion.
69 changes: 69 additions & 0 deletions contract/contracts/LinkedList.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

bytes32 constant PRE_HEAD_ADDR = keccak256("preHead");
bytes32 constant POST_TAIL_ADDR = keccak256("postTail");

struct Node {
bytes32 val;
bytes32 next;
bytes32 prev;
}
struct LinkedList {
mapping (bytes32 => Node) nodes;
uint256 size;
}

library LinkedListInterface {
function init(LinkedList storage self) public {
Node storage preHead = self.nodes[PRE_HEAD_ADDR];
Node storage postTail = self.nodes[POST_TAIL_ADDR];
if(preHead.next != 0) return;
preHead.next = POST_TAIL_ADDR;
postTail.prev = PRE_HEAD_ADDR;
preHead.val = PRE_HEAD_ADDR;
postTail.val = POST_TAIL_ADDR;
}
function insertTail(LinkedList storage self, bytes32 val) public {
bool uniqVal = self.nodes[val].next == 0;
require(uniqVal, "Unique values only !");

Node storage postTail = self.nodes[POST_TAIL_ADDR];
Node storage prev = self.nodes[postTail.prev];
Node memory node = Node(val, prev.next, postTail.prev);
bytes32 nodeKey = node.val;
prev.next = nodeKey;
postTail.prev = nodeKey;
self.nodes[val] = node;
self.size += 1;
}

function getHead(LinkedList storage self) public view returns (bytes32) {
require(self.size>0, "no items");
Node storage preHead = self.nodes[PRE_HEAD_ADDR];
bytes32 headAddr = preHead.next;
Node storage head = self.nodes[headAddr];
return head.val;
}

function removeHead(LinkedList storage self) external {
require(self.size>0, "no items");
Node storage prev = self.nodes[PRE_HEAD_ADDR];
bytes32 headAddr = prev.next;
Node storage head = self.nodes[headAddr];
Node storage next = self.nodes[head.next];
prev.next = head.next;
next.prev = head.prev;
self.size -= 1;
}

function remove(LinkedList storage self, bytes32 val) external {
require(self.size>0, "no items");
Node storage node = self.nodes[val];
Node storage prev = self.nodes[node.prev];
Node storage next = self.nodes[node.next];
prev.next = node.next;
next.prev = node.prev;
self.size -= 1;
}
}
113 changes: 113 additions & 0 deletions contract/contracts/Mailbox.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {UserMailbox, UserMailboxInterface, Message} from "./UserMailbox.sol";

/**
* @title Mailbox
* @dev A contract for intermediate message exchange between parties.
*/
contract Mailbox {
/// account who deployed the contract
address private immutable owner;

/// @dev Per user Mailbox holding all messages sent by different senders
mapping (address => UserMailbox) mailboxes;

/// @dev Max number of messages allowed for a single Mailbox (sender,recipient)
uint256 constant public MAX_MESSAGES_PER_MAILBOX = 10;

/// @notice Emitted when mailbox message count changes, new message arrival or message marked as read
/// @param sender The address of the message sender
/// @param recipient The address of the message recipient
/// @param messagesCount Total number of messages in the Mailbox for (sender,recipient)
/// @param timestamp Time when operation occurred
event MailboxUpdated(address indexed sender, address indexed recipient, uint messagesCount, uint256 timestamp);

/// @notice Raised on attemt to write a messages to a full Mailbox
error MailboxIsFull();

using UserMailboxInterface for UserMailbox;

constructor() {
owner = msg.sender;
}

/**
* @notice Writes a messages to a dedicated Mailbox for (sender,recipient)
* @param message The message to write
* @param recipient Message recipient address
*/
function writeMessage(bytes calldata message, address recipient) external {
UserMailbox storage mailbox = mailboxes[recipient];
uint256 msgCount = mailbox.countMessagesFrom(msg.sender);
if (msgCount == MAX_MESSAGES_PER_MAILBOX) revert MailboxIsFull();

Message memory _msg = Message({
sender: msg.sender,
data: message,
sentAt: block.timestamp
});
mailbox.writeMessage(_msg);

emit MailboxUpdated(msg.sender, recipient, msgCount+1, block.timestamp);
}

/**
* @notice Provides a message to its recipient from the specified sender
* @param sender Sender address
* @return msgId Message ID
* @return data The message
* @return sentAt Timestamp when the message was written
*/
function readMessage(address sender) external view
returns (bytes32 msgId, bytes memory data, uint256 sentAt) {

UserMailbox storage mailbox = mailboxes[msg.sender];
uint256 msgCount = mailbox.countMessagesFrom(sender);
if (msgCount == 0) {
bytes memory zero;
return (bytes32(0), zero, 0);
}
(bytes32 _msgId, Message memory _msg) = mailbox.readMessageFrom(sender);
msgId = _msgId;
data = _msg.data;
sentAt = _msg.sentAt;
}

/**
* @notice Allows a recipient to read a message without specifying a sender.
* Recipient is given next sender message after each read confirmation done by markMessageRead
* @return msgId Message ID
* @return sender address
* @return data The message
* @return sentAt Timestamp when the message was written
*/
function readMessageNextSender() external view
returns (bytes32 msgId, address sender, bytes memory data, uint256 sentAt) {
UserMailbox storage mailbox = mailboxes[msg.sender];
uint256 msgCount = mailbox.countSenders();
if (msgCount == 0) {
bytes memory zero;
return (bytes32(0), address(0), zero, 0);
}
Message storage _msg;
(msgId, _msg) = mailbox.readMessageNextSender();
sender = _msg.sender;
data = _msg.data;
sentAt = _msg.sentAt;
}

/**
* Marks a top message as read making the next message available for reading
* @param msgId ID of the read message
* @return moreMessages whether other message available from the same sender
*/
function markMessageRead(bytes32 msgId) external returns (bool moreMessages) {
UserMailbox storage mailbox = mailboxes[msg.sender];
Message storage _msg = mailbox.getMessage(msgId);
uint256 msgCount = mailbox.countMessagesFrom(_msg.sender);
emit MailboxUpdated(_msg.sender, msg.sender, msgCount-1, block.timestamp);
return mailbox.markMessageRead(msgId);
}
}
82 changes: 82 additions & 0 deletions contract/contracts/UserMailbox.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {LinkedList, LinkedListInterface} from "./LinkedList.sol";

struct Message {
address sender;
uint256 sentAt;
bytes data;
}
struct UserMailbox {
mapping (bytes32 => Message) messages;
mapping (bytes32 => LinkedList) orderedMessageLists;
uint256 totalMessagesCount;
}

bytes32 constant NEXT_SENDER_LIST_ID = keccak256("nextSender");

using LinkedListInterface for LinkedList;

error MessageNotFound();

library UserMailboxInterface {
function writeMessage(UserMailbox storage self, Message memory _msg) public {
bytes32 messageId = keccak256(abi.encode(_msg));
self.messages[messageId] = _msg;

LinkedList storage list = self.orderedMessageLists[keccak256(abi.encode(_msg.sender))];
list.init();
list.insertTail(messageId);
bool isFirstSenderMsg = list.size == 1;

if (isFirstSenderMsg) {
LinkedList storage nextSenderList = self.orderedMessageLists[NEXT_SENDER_LIST_ID];
nextSenderList.init();
nextSenderList.insertTail(messageId);
}
}

function readMessageFrom(UserMailbox storage self, address sender) public view returns (bytes32, Message storage) {
LinkedList storage list = self.orderedMessageLists[keccak256(abi.encode(sender))];
bytes32 valHash = list.getHead();
return (valHash, self.messages[valHash]);
}

function readMessageNextSender(UserMailbox storage self) public view returns (bytes32, Message storage) {
LinkedList storage list = self.orderedMessageLists[NEXT_SENDER_LIST_ID];
bytes32 valHash = list.getHead();
return (valHash, self.messages[valHash]);
}

function getMessage(UserMailbox storage self, bytes32 msgId) internal view returns (Message storage) {
Message storage _msg = self.messages[msgId];
if(_msg.sentAt == 0) {
revert MessageNotFound();
}
return _msg;
}

function countMessagesFrom(UserMailbox storage self, address sender) public view returns (uint256) {
return self.orderedMessageLists[keccak256(abi.encode(sender))].size;
}

function countSenders(UserMailbox storage self) public view returns (uint256) {
return self.orderedMessageLists[NEXT_SENDER_LIST_ID].size;
}

function markMessageRead(UserMailbox storage self, bytes32 messageId) public returns (bool moreMessages) {
Message storage _msg = self.messages[messageId];
LinkedList storage list = self.orderedMessageLists[keccak256(abi.encode(_msg.sender))];
list.remove(messageId);
self.messages[messageId].sentAt=0;

LinkedList storage nextSenderList = self.orderedMessageLists[NEXT_SENDER_LIST_ID];
nextSenderList.init();
nextSenderList.remove(messageId);
if(list.size>0) {
nextSenderList.insertTail(list.getHead());
}
return list.size > 0;
}
}
24 changes: 24 additions & 0 deletions contract/ignition/modules/Mailbox.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
const { buildModule } = require("@nomicfoundation/hardhat-ignition/modules");


const userMailboxModule = buildModule("UserMailboxModule", (m) => {
const linkedList = m.library("LinkedListInterface");
const userMailbox = m.contract("UserMailboxInterface", [], {
libraries: {
LinkedListInterface: linkedList,
}
});

return { userMailbox };
});

module.exports = buildModule("MailboxModule", (m) => {
const {userMailbox} = m.useModule(userMailboxModule);
const contract = m.contract("Mailbox", [], {
libraries: {
UserMailboxInterface: userMailbox
}
});

return { contract };
});
Loading

0 comments on commit 5460c6c

Please sign in to comment.