packages/contracts-core/contracts/client/PingPongClient.sol
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
// ══════════════════════════════ LIBRARY IMPORTS ══════════════════════════════
import {TypeCasts} from "../libs/TypeCasts.sol";
// ═════════════════════════════ INTERNAL IMPORTS ══════════════════════════════
import {MessageRecipient} from "./MessageRecipient.sol";
contract PingPongClient is MessageRecipient {
using TypeCasts for address;
struct PingPongMessage {
uint256 pingId;
bool isPing;
uint16 counter;
}
// ══════════════════════════════════════════════════ STORAGE ══════════════════════════════════════════════════════
uint256 public random;
/// @notice Amount of "Ping" messages sent.
uint256 public pingsSent;
/// @notice Amount of "Ping" messages received.
/// Every received Ping message leads to sending a Pong message back to initial sender.
uint256 public pingsReceived;
/// @notice Amount of "Pong" messages received.
/// When all messages are delivered, should be equal to `pingsSent`
uint256 public pongsReceived;
// ══════════════════════════════════════════════════ EVENTS ═══════════════════════════════════════════════════════
/// @notice Emitted when a Ping message is sent.
/// Triggered externally, or by receveing a Pong message with instructions to do more pings.
event PingSent(uint256 pingId);
/// @notice Emitted when a Ping message is received.
/// Will always send a Pong message back.
event PingReceived(uint256 pingId);
/// @notice Emitted when a Pong message is sent.
/// Triggered whenever a Ping message is received.
event PongSent(uint256 pingId);
/// @notice Emitted when a Pong message is received.
/// Will initiate a new Ping, if the counter in the message is non-zero.
event PongReceived(uint256 pingId);
// ════════════════════════════════════════════════ CONSTRUCTOR ════════════════════════════════════════════════════
constructor(address origin_, address destination_) MessageRecipient(origin_, destination_) {
// Initiate "random" value
random = uint256(keccak256(abi.encode(block.number)));
}
// ═══════════════════════════════════════════════ MESSAGE LOGIC ═══════════════════════════════════════════════════
function doPings(uint16 pingCount, uint32 destination_, address recipient, uint16 counter) external {
for (uint256 i = 0; i < pingCount; ++i) {
_ping(destination_, recipient.addressToBytes32(), counter);
}
}
/// @notice Send a Ping message to destination chain.
/// Upon receiving a Ping, a Pong message will be sent back.
/// If `counter > 0`, this process will be repeated when the Pong message is received.
/// @param destination_ Chain to send Ping message to
/// @param recipient Recipient of Ping message
/// @param counter Additional amount of Ping-Pong rounds to conclude
function doPing(uint32 destination_, address recipient, uint16 counter) external {
_ping(destination_, recipient.addressToBytes32(), counter);
}
// ═══════════════════════════════════════════════════ VIEWS ═══════════════════════════════════════════════════════
function nextOptimisticPeriod() public view returns (uint32 period) {
// Use random optimistic period up to one minute
return uint32(random % 1 minutes);
}
// ═════════════════════════════════════ INTERNAL LOGIC: RECEIVE MESSAGES ══════════════════════════════════════════
/// @inheritdoc MessageRecipient
function _receiveBaseMessageUnsafe(uint32 origin_, uint32, bytes32 sender, uint256, uint32, bytes memory content)
internal
override
{
PingPongMessage memory message = abi.decode(content, (PingPongMessage));
if (message.isPing) {
// Ping is received
++pingsReceived;
emit PingReceived(message.pingId);
// Send Pong back
_pong(origin_, sender, message);
} else {
// Pong is received
++pongsReceived;
emit PongReceived(message.pingId);
// Send extra ping, if initially requested
if (message.counter != 0) {
_ping(origin_, sender, message.counter - 1);
}
}
}
// ═══════════════════════════════════════ INTERNAL LOGIC: SEND MESSAGES ═══════════════════════════════════════════
/// @dev Returns a random optimistic period value from 0 to 59 seconds.
function _optimisticPeriod() internal returns (uint32 period) {
// Use random optimistic period up to one minute
period = nextOptimisticPeriod();
// Adjust "random" value
random = uint256(keccak256(abi.encode(random)));
}
/**
* @dev Send a "Ping" or "Pong" message.
* @param destination_ Domain of destination chain
* @param recipient Message recipient on destination chain
* @param message Ping-pong message
*/
function _sendMessage(uint32 destination_, bytes32 recipient, PingPongMessage memory message) internal {
// TODO: this probably shouldn't be hardcoded
MessageRequest memory request = MessageRequest({gasDrop: 0, gasLimit: 500_000, version: 0});
bytes memory content = abi.encode(message);
_sendBaseMessage({
destination_: destination_,
recipient: recipient,
optimisticPeriod: _optimisticPeriod(),
tipsValue: 0,
request: request,
content: content
});
}
/// @dev Initiate a new Ping-Pong round.
function _ping(uint32 destination_, bytes32 recipient, uint16 counter) internal {
uint256 pingId = pingsSent++;
_sendMessage(destination_, recipient, PingPongMessage({pingId: pingId, isPing: true, counter: counter}));
emit PingSent(pingId);
}
/// @dev Send a Pong message back.
function _pong(uint32 destination_, bytes32 recipient, PingPongMessage memory message) internal {
_sendMessage(
destination_, recipient, PingPongMessage({pingId: message.pingId, isPing: false, counter: message.counter})
);
emit PongSent(message.pingId);
}
}