1use agent_wire_foundation::{
2 CreditAmount, CrossGraphRef, EndpointUrl, HandlePath, SettlementIntent, TunnelUrl,
3};
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
7pub struct RelayOfferId(pub String);
8
9#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
10pub struct PathLeaseId(pub String);
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
13pub enum HopCapability {
14 HttpTunnel,
15 EventStream,
16 StoreAndForward,
17}
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
20pub enum PrivacyTier {
21 Direct,
22 Shielded,
23 Onion,
24}
25
26#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
27pub struct RotationPolicy {
28 pub rotate_after_seconds: u64,
29 pub max_reuses: u32,
30}
31
32#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
33pub struct RelayOffer {
34 pub offer_id: RelayOfferId,
35 pub operator: HandlePath,
36 pub ingress: EndpointUrl,
37 pub egress: TunnelUrl,
38 pub capabilities: Vec<HopCapability>,
39 pub privacy_tiers: Vec<PrivacyTier>,
40 pub price_per_hop: CreditAmount,
41 pub settlement: SettlementIntent,
42}
43
44#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
45pub struct PathLeaseRequest {
46 pub requester: HandlePath,
47 pub desired_hops: u8,
48 pub required_capabilities: Vec<HopCapability>,
49 pub privacy_tier: PrivacyTier,
50 pub rotation: RotationPolicy,
51 pub max_price: CreditAmount,
52}
53
54#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
55pub struct RelayHop {
56 pub operator: HandlePath,
57 pub ingress: EndpointUrl,
58 pub egress: TunnelUrl,
59 pub capabilities: Vec<HopCapability>,
60 pub price: CreditAmount,
61}
62
63#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
64pub struct RelayPathLease {
65 pub lease_id: PathLeaseId,
66 pub requester: HandlePath,
67 pub hops: Vec<RelayHop>,
68 pub privacy_tier: PrivacyTier,
69 pub rotation: RotationPolicy,
70 pub settlement: SettlementIntent,
71}
72
73#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
74pub struct PerHopSettlement {
75 pub lease_id: PathLeaseId,
76 pub hop_index: u16,
77 pub payee: HandlePath,
78 pub amount: CreditAmount,
79 pub receipt_ref: Option<CrossGraphRef>,
80}
81
82pub trait RelayMarket {
83 type Error;
84
85 fn publish_offer(&self, offer: RelayOffer) -> Result<RelayOfferId, Self::Error>;
86 fn lease_path(&self, request: PathLeaseRequest) -> Result<RelayPathLease, Self::Error>;
87 fn rotate_path(
88 &self,
89 lease_id: PathLeaseId,
90 policy: RotationPolicy,
91 ) -> Result<RelayPathLease, Self::Error>;
92 fn settle_hop(&self, settlement: PerHopSettlement) -> Result<(), Self::Error>;
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98
99 #[test]
100 fn relay_offer_declares_capabilities_privacy_and_settlement() {
101 let offer = RelayOffer {
102 offer_id: RelayOfferId("relay-offer-1".to_owned()),
103 operator: HandlePath::new(["agent", "playful", "relay"]).unwrap(),
104 ingress: EndpointUrl::parse("https://relay.example/ingress").unwrap(),
105 egress: TunnelUrl::parse("https://relay.example/tunnel").unwrap(),
106 capabilities: vec![HopCapability::HttpTunnel, HopCapability::EventStream],
107 privacy_tiers: vec![PrivacyTier::Shielded, PrivacyTier::Onion],
108 price_per_hop: CreditAmount::from_sats(25),
109 settlement: SettlementIntent {
110 max_price: CreditAmount::from_sats(100),
111 escrow_required: true,
112 },
113 };
114
115 assert!(offer.capabilities.contains(&HopCapability::EventStream));
116 assert!(offer.privacy_tiers.contains(&PrivacyTier::Onion));
117 assert_eq!(offer.price_per_hop.as_sats(), 25);
118 }
119
120 #[test]
121 fn per_hop_settlement_can_cite_receipt_ref() {
122 let settlement = PerHopSettlement {
123 lease_id: PathLeaseId("lease-1".to_owned()),
124 hop_index: 1,
125 payee: HandlePath::new(["agent", "playful", "relay"]).unwrap(),
126 amount: CreditAmount::from_sats(12),
127 receipt_ref: Some("playful/122/relay/1".parse().unwrap()),
128 };
129
130 assert_eq!(settlement.hop_index, 1);
131 assert_eq!(
132 settlement.receipt_ref.unwrap().to_string(),
133 "playful/122/relay/1"
134 );
135 }
136}