Skip to main content

agent_wire_storage_market/
lib.rs

1use agent_wire_foundation::{
2    CreditAmount, CrossGraphRef, GraphSlug, HandlePath, PriceCurve, SettlementIntent,
3};
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
7pub struct StorageOfferId(pub String);
8
9#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
10pub struct PinCommitmentId(pub String);
11
12#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
13pub struct RetrievalRequestId(pub String);
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
16pub struct ReplicationFactor(pub u16);
17
18#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
19pub struct RetentionPolicy {
20    pub minimum_seconds: u64,
21    pub renew_before_expiry_seconds: Option<u64>,
22}
23
24#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
25pub struct CapacityAllocation {
26    pub graph: GraphSlug,
27    pub reserved_bytes: u64,
28}
29
30#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
31pub struct StorageOffer {
32    pub offer_id: StorageOfferId,
33    pub provider: HandlePath,
34    pub capacity_bytes: u64,
35    pub capacity_allocation: Vec<CapacityAllocation>,
36    pub price: PriceCurve,
37    pub replication: ReplicationFactor,
38    pub retention: RetentionPolicy,
39    pub settlement: SettlementIntent,
40}
41
42#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
43pub struct PinCommitmentRequest {
44    pub offer_id: StorageOfferId,
45    pub requester: HandlePath,
46    pub content_ref: CrossGraphRef,
47    pub bytes: u64,
48    pub replication: ReplicationFactor,
49    pub retention: RetentionPolicy,
50    pub settlement: SettlementIntent,
51}
52
53#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
54pub struct PinCommitment {
55    pub commitment_id: PinCommitmentId,
56    pub request: PinCommitmentRequest,
57    pub provider: HandlePath,
58}
59
60#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
61pub struct RetrievalRequest {
62    pub request_id: RetrievalRequestId,
63    pub commitment_id: PinCommitmentId,
64    pub requester: HandlePath,
65    pub content_ref: CrossGraphRef,
66    pub max_price: CreditAmount,
67}
68
69#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
70pub struct RetrievalReceipt {
71    pub request_id: RetrievalRequestId,
72    pub content_ref: CrossGraphRef,
73    pub served_by: HandlePath,
74    pub bytes_served: u64,
75    pub charged: CreditAmount,
76}
77
78pub trait StorageMarket {
79    type Error;
80
81    fn publish_offer(&self, offer: StorageOffer) -> Result<StorageOfferId, Self::Error>;
82    fn commit_pin(&self, request: PinCommitmentRequest) -> Result<PinCommitment, Self::Error>;
83    fn retrieve(&self, request: RetrievalRequest) -> Result<RetrievalReceipt, Self::Error>;
84    fn renew_retention(
85        &self,
86        commitment_id: PinCommitmentId,
87        retention: RetentionPolicy,
88    ) -> Result<PinCommitment, Self::Error>;
89}
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94
95    #[test]
96    fn storage_offer_carries_capacity_retention_and_settlement() {
97        let offer = StorageOffer {
98            offer_id: StorageOfferId("offer-1".to_owned()),
99            provider: HandlePath::new(["agent", "playful", "storage"]).unwrap(),
100            capacity_bytes: 1_000_000,
101            capacity_allocation: vec![CapacityAllocation {
102                graph: GraphSlug::new("kitty").unwrap(),
103                reserved_bytes: 250_000,
104            }],
105            price: PriceCurve {
106                base: CreditAmount::from_sats(10),
107                per_unit: CreditAmount::from_sats(2),
108            },
109            replication: ReplicationFactor(3),
110            retention: RetentionPolicy {
111                minimum_seconds: 86_400,
112                renew_before_expiry_seconds: Some(3_600),
113            },
114            settlement: SettlementIntent {
115                max_price: CreditAmount::from_sats(1_000),
116                escrow_required: true,
117            },
118        };
119
120        assert_eq!(offer.capacity_allocation[0].graph.as_str(), "kitty");
121        assert_eq!(offer.replication, ReplicationFactor(3));
122        assert!(offer.settlement.escrow_required);
123    }
124
125    #[test]
126    fn pin_commitment_request_names_cross_graph_content() {
127        let request = PinCommitmentRequest {
128            offer_id: StorageOfferId("offer-1".to_owned()),
129            requester: HandlePath::new(["agent", "playful", "kramer"]).unwrap(),
130            content_ref: "playful/122/storage/1".parse().unwrap(),
131            bytes: 512,
132            replication: ReplicationFactor(2),
133            retention: RetentionPolicy {
134                minimum_seconds: 600,
135                renew_before_expiry_seconds: None,
136            },
137            settlement: SettlementIntent {
138                max_price: CreditAmount::from_sats(40),
139                escrow_required: false,
140            },
141        };
142
143        assert_eq!(request.content_ref.to_string(), "playful/122/storage/1");
144        assert_eq!(request.bytes, 512);
145    }
146}