// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@eas/contracts/resolver/SchemaResolver.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract DeliveryEscrow is SchemaResolver, ReentrancyGuard {
address public astralSigner;
struct Delivery {
address buyer;
address courier;
bytes32 destinationUID; // Location record of delivery address
uint256 amount;
uint256 radius; // Acceptable radius in meters
uint256 deadline;
bool completed;
bool refunded;
}
mapping(bytes32 => Delivery) public deliveries;
mapping(bytes32 => bool) public usedAttestations;
event DeliveryCreated(bytes32 indexed deliveryId, address buyer, uint256 amount);
event DeliveryCompleted(bytes32 indexed deliveryId, address courier);
event DeliveryRefunded(bytes32 indexed deliveryId, address buyer);
constructor(IEAS eas, address _astralSigner) SchemaResolver(eas) {
astralSigner = _astralSigner;
}
// Buyer creates delivery escrow
function createDelivery(
bytes32 deliveryId,
address courier,
bytes32 destinationUID,
uint256 radiusMeters,
uint256 deadline
) external payable {
require(msg.value > 0, "Must send payment");
require(deliveries[deliveryId].buyer == address(0), "Already exists");
require(deadline > block.timestamp, "Invalid deadline");
deliveries[deliveryId] = Delivery({
buyer: msg.sender,
courier: courier,
destinationUID: destinationUID,
amount: msg.value,
radius: radiusMeters * 100, // Convert to cm
deadline: deadline,
completed: false,
refunded: false
});
emit DeliveryCreated(deliveryId, msg.sender, msg.value);
}
function onAttest(
Attestation calldata attestation,
uint256 /*value*/
) internal override returns (bool) {
require(attestation.attester == astralSigner, "Not from Astral");
require(!usedAttestations[attestation.uid], "Already used");
usedAttestations[attestation.uid] = true;
// Decode the signed result
(
bool isWithinRadius,
bytes32[] memory inputRefs,
uint64 timestamp,
string memory operation
) = abi.decode(
attestation.data,
(bool, bytes32[], uint64, string)
);
// Verify operation type
require(
keccak256(bytes(operation)) == keccak256(bytes("within")),
"Wrong operation"
);
// Extract delivery ID from recipient field
bytes32 deliveryId = bytes32(uint256(uint160(attestation.recipient)));
Delivery storage delivery = deliveries[deliveryId];
require(delivery.buyer != address(0), "Delivery not found");
require(!delivery.completed, "Already completed");
require(!delivery.refunded, "Already refunded");
require(block.timestamp <= delivery.deadline, "Deadline passed");
// Verify correct destination was checked
require(inputRefs.length >= 2, "Invalid inputs");
require(inputRefs[1] == delivery.destinationUID, "Wrong destination");
// Verify courier is within radius
require(isWithinRadius, "Not at delivery location");
// Verify timestamp is recent
require(timestamp > block.timestamp - 30 minutes, "Result too old");
// Complete delivery
delivery.completed = true;
// Release funds to courier
(bool success, ) = delivery.courier.call{value: delivery.amount}("");
require(success, "Transfer failed");
emit DeliveryCompleted(deliveryId, delivery.courier);
return true;
}
// Buyer can refund after deadline
function refund(bytes32 deliveryId) external nonReentrant {
Delivery storage delivery = deliveries[deliveryId];
require(msg.sender == delivery.buyer, "Not buyer");
require(!delivery.completed, "Already completed");
require(!delivery.refunded, "Already refunded");
require(block.timestamp > delivery.deadline, "Deadline not passed");
delivery.refunded = true;
(bool success, ) = delivery.buyer.call{value: delivery.amount}("");
require(success, "Refund failed");
emit DeliveryRefunded(deliveryId, delivery.buyer);
}
function onRevoke(Attestation calldata, uint256) internal pure override returns (bool) {
return false;
}
}