1use anyhow::anyhow;
7use diesel::{
8 backend::Backend,
9 deserialize::{self, FromSql},
10 serialize::{self, IsNull, Output, ToSql},
11 sql_types::Binary,
12 sqlite::Sqlite,
13 AsExpression, FromSqlRow, Identifiable, Insertable, Queryable, Selectable,
14};
15use homestar_invocation::{
16 authority::{Issuer, UcanPrf},
17 consts,
18 ipld::{DagCborRef, DagJson},
19 task, Pointer, Receipt as InvocationReceipt,
20};
21use homestar_wasm::io::Arg;
22use libipld::{cbor::DagCborCodec, cid::Cid, prelude::Codec, serde::from_ipld, Ipld};
23use semver::Version;
24use serde::{Deserialize, Serialize};
25use std::{collections::BTreeMap, fmt};
26
27pub(crate) mod metadata;
28
29pub const VERSION_KEY: &str = "version";
31pub const RECEIPT_TAG: &str = "ipvm/receipt";
33
34const CID_KEY: &str = "cid";
35const INSTRUCTION_KEY: &str = "instruction";
36const RAN_KEY: &str = "ran";
37const OUT_KEY: &str = "out";
38const ISSUER_KEY: &str = "iss";
39const METADATA_KEY: &str = "meta";
40const PROOF_KEY: &str = "prf";
41
42#[derive(Debug, Clone, PartialEq, Queryable, Insertable, Identifiable, Selectable)]
50#[diesel(table_name = crate::db::schema::receipts, primary_key(cid))]
51pub struct Receipt {
52 cid: Pointer,
53 ran: Pointer,
54 instruction: Pointer,
55 out: task::Result<Ipld>,
56 meta: LocalIpld,
57 issuer: Option<Issuer>,
58 prf: UcanPrf,
59 version: String,
60}
61
62impl fmt::Display for Receipt {
63 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
64 write!(
65 f,
66 "Receipt: [cid: {}, instruction: {}, ran: {}, output: {:?}, metadata: {:?}, issuer: {:?}, version: {}]",
67 self.cid, self.instruction, self.ran, self.out, self.meta.0, self.issuer, self.version,
68 )
69 }
70}
71
72impl Receipt {
73 pub fn new(
75 cid: Cid,
76 instruction: Pointer,
77 invocation_receipt: &InvocationReceipt<Ipld>,
78 ) -> Self {
79 Self {
80 cid: Pointer::new(cid),
81 ran: invocation_receipt.ran().to_owned(),
82 instruction,
83 out: invocation_receipt.out().to_owned(),
84 meta: LocalIpld(invocation_receipt.meta().to_owned()),
85 issuer: invocation_receipt.issuer().to_owned(),
86 prf: invocation_receipt.prf().to_owned(),
87 version: consts::INVOCATION_VERSION.to_string(),
88 }
89 }
90
91 pub fn try_with(
97 instruction: Pointer,
98 invocation_receipt: &InvocationReceipt<Ipld>,
99 ) -> anyhow::Result<Self> {
100 let cid = invocation_receipt.to_cid()?;
101 Ok(Receipt::new(cid, instruction, invocation_receipt))
102 }
103
104 pub fn invocation_capsule(
107 invocation_receipt: &InvocationReceipt<Ipld>,
108 ) -> anyhow::Result<Vec<u8>> {
109 let receipt_ipld = Ipld::from(invocation_receipt);
110 let capsule = if let Ipld::Map(mut map) = receipt_ipld {
111 map.insert(VERSION_KEY.into(), consts::INVOCATION_VERSION.into());
112 Ok(Ipld::Map(BTreeMap::from([(
113 RECEIPT_TAG.into(),
114 Ipld::Map(map),
115 )])))
116 } else {
117 Err(anyhow!("receipt to Ipld conversion is not a map"))
118 }?;
119
120 DagCborCodec.encode(&capsule)
121 }
122
123 pub fn meta(&self) -> &Ipld {
125 self.meta.inner()
126 }
127
128 pub fn set_meta(&mut self, meta: Ipld) {
130 self.meta = LocalIpld(meta)
131 }
132
133 pub fn cid(&self) -> Cid {
135 self.cid.cid()
136 }
137
138 pub fn cid_as_string(&self) -> String {
140 self.cid().to_string()
141 }
142
143 pub fn cid_as_bytes(&self) -> Vec<u8> {
145 self.cid().to_bytes()
146 }
147
148 pub fn instruction(&self) -> &Pointer {
152 &self.instruction
153 }
154
155 pub fn instruction_cid_as_bytes(&self) -> Vec<u8> {
157 self.instruction.cid().to_bytes()
158 }
159
160 pub fn ran(&self) -> String {
162 self.ran.to_string()
163 }
164
165 pub fn output(&self) -> &task::Result<Ipld> {
167 &self.out
168 }
169
170 pub fn output_as_arg(&self) -> task::Result<Arg> {
172 match self.out.to_owned() {
173 task::Result::Ok(res) => task::Result::Ok(res.into()),
174 task::Result::Error(res) => task::Result::Error(res.into()),
175 task::Result::Just(res) => task::Result::Just(res.into()),
176 }
177 }
178
179 pub fn output_encoded(&self) -> anyhow::Result<Vec<u8>> {
181 let ipld = Ipld::from(self.out.to_owned());
182 DagCborCodec.encode(&ipld)
183 }
184
185 pub fn version(&self) -> Result<Version, semver::Error> {
187 Version::parse(&self.version)
188 }
189}
190
191impl TryFrom<Receipt> for Vec<u8> {
192 type Error = anyhow::Error;
193
194 fn try_from(receipt: Receipt) -> Result<Self, Self::Error> {
195 let receipt_ipld = Ipld::from(receipt);
196 DagCborCodec.encode(&receipt_ipld)
197 }
198}
199
200impl TryFrom<Vec<u8>> for Receipt {
201 type Error = anyhow::Error;
202
203 fn try_from(bytes: Vec<u8>) -> Result<Self, Self::Error> {
204 let ipld: Ipld = DagCborCodec.decode(&bytes)?;
205 ipld.try_into()
206 }
207}
208
209impl From<Receipt> for InvocationReceipt<Ipld> {
210 fn from(receipt: Receipt) -> Self {
211 InvocationReceipt::new(
212 receipt.ran,
213 receipt.out,
214 receipt.meta.0,
215 receipt.issuer,
216 receipt.prf,
217 )
218 }
219}
220
221impl From<&Receipt> for InvocationReceipt<Ipld> {
222 fn from(receipt: &Receipt) -> Self {
223 InvocationReceipt::new(
224 receipt.ran.clone(),
225 receipt.out.clone(),
226 receipt.meta.0.clone(),
227 receipt.issuer.clone(),
228 receipt.prf.clone(),
229 )
230 }
231}
232
233impl From<Receipt> for Ipld {
234 fn from(receipt: Receipt) -> Self {
235 Ipld::Map(BTreeMap::from([
236 (CID_KEY.into(), receipt.cid.into()),
237 (RAN_KEY.into(), receipt.ran.into()),
238 (INSTRUCTION_KEY.into(), receipt.instruction.into()),
239 (OUT_KEY.into(), receipt.out.into()),
240 (METADATA_KEY.into(), receipt.meta.0),
241 (
242 ISSUER_KEY.into(),
243 receipt
244 .issuer
245 .map(|issuer| issuer.to_string().into())
246 .unwrap_or(Ipld::Null),
247 ),
248 (PROOF_KEY.into(), receipt.prf.into()),
249 (VERSION_KEY.into(), receipt.version.into()),
250 ]))
251 }
252}
253
254impl TryFrom<Ipld> for Receipt {
255 type Error = anyhow::Error;
256
257 fn try_from(ipld: Ipld) -> Result<Self, Self::Error> {
258 let map = from_ipld::<BTreeMap<String, Ipld>>(ipld)?;
259 let cid = from_ipld(
260 map.get(CID_KEY)
261 .ok_or_else(|| anyhow!("missing {CID_KEY}"))?
262 .to_owned(),
263 )?;
264 let ran = map
265 .get(RAN_KEY)
266 .ok_or_else(|| anyhow!("missing {RAN_KEY}"))?
267 .try_into()?;
268 let instruction = map
269 .get(INSTRUCTION_KEY)
270 .ok_or_else(|| anyhow!("missing {INSTRUCTION_KEY}"))?
271 .try_into()?;
272 let out = map
273 .get(OUT_KEY)
274 .ok_or_else(|| anyhow!("missing {OUT_KEY}"))?;
275 let meta = map
276 .get(METADATA_KEY)
277 .ok_or_else(|| anyhow!("missing {METADATA_KEY}"))?;
278 let issuer = map
279 .get(ISSUER_KEY)
280 .and_then(|ipld| match ipld {
281 Ipld::Null => None,
282 ipld => Some(ipld),
283 })
284 .and_then(|ipld| from_ipld(ipld.to_owned()).ok())
285 .map(Issuer::new);
286 let prf = map
287 .get(PROOF_KEY)
288 .ok_or_else(|| anyhow!("missing {PROOF_KEY}"))?;
289 let version = from_ipld(
290 map.get(VERSION_KEY)
291 .ok_or_else(|| anyhow!("missing {VERSION_KEY}"))?
292 .to_owned(),
293 )?;
294
295 Ok(Receipt {
296 cid: Pointer::new(cid),
297 ran,
298 instruction,
299 out: task::Result::try_from(out)?,
300 meta: LocalIpld(meta.to_owned()),
301 issuer,
302 prf: UcanPrf::try_from(prf)?,
303 version,
304 })
305 }
306}
307
308impl DagJson for Receipt where Ipld: From<Receipt> {}
309
310#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, AsExpression, FromSqlRow)]
312#[diesel(sql_type = Binary)]
313pub struct LocalIpld(Ipld);
314
315impl LocalIpld {
316 pub fn into_inner(self) -> Ipld {
318 self.0
319 }
320
321 pub fn inner(&self) -> &Ipld {
323 &self.0
324 }
325}
326
327impl ToSql<Binary, Sqlite> for LocalIpld
328where
329 [u8]: ToSql<Binary, Sqlite>,
330{
331 fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Sqlite>) -> serialize::Result {
332 out.set_value(DagCborCodec.encode(&self.0)?);
333 Ok(IsNull::No)
334 }
335}
336
337impl<DB> FromSql<Binary, DB> for LocalIpld
338where
339 DB: Backend,
340 *const [u8]: FromSql<Binary, DB>,
341{
342 fn from_sql(bytes: DB::RawValue<'_>) -> deserialize::Result<Self> {
343 let raw_bytes = <*const [u8] as FromSql<Binary, DB>>::from_sql(bytes)?;
344 let raw_bytes: &[u8] = unsafe { &*raw_bytes };
345 let decoded = DagCborCodec.decode(raw_bytes)?;
346 Ok(LocalIpld(decoded))
347 }
348}
349
350#[cfg(test)]
351mod test {
352 use super::*;
353 use crate::{
354 db::{schema, Database},
355 settings::Settings,
356 test_utils::{self, db::MemoryDb},
357 };
358 use diesel::prelude::*;
359
360 #[test]
361 fn invocation_into_receipt() {
362 let (invocation, receipt) = test_utils::receipt::receipts();
363 assert_eq!(invocation.ran().to_string(), receipt.ran());
364 assert_eq!(invocation.out(), receipt.output());
365 assert_eq!(invocation.meta(), &receipt.meta.0);
366 assert_eq!(invocation.issuer(), &receipt.issuer);
367 assert_eq!(invocation.prf(), &receipt.prf);
368 assert_eq!(invocation.to_cid().unwrap(), receipt.cid());
369
370 let output_bytes = DagCborCodec
371 .encode::<Ipld>(&invocation.out().clone().into())
372 .unwrap();
373 assert_eq!(output_bytes, receipt.output_encoded().unwrap());
374
375 let receipt_from_invocation =
376 Receipt::try_with(receipt.instruction.clone(), &invocation).unwrap();
377 assert_eq!(receipt_from_invocation, receipt);
378
379 let invocation_from_receipt = InvocationReceipt::try_from(receipt).unwrap();
380 assert_eq!(invocation_from_receipt, invocation);
381 }
382
383 #[test]
384 fn receipt_sql_roundtrip() {
385 let mut conn = MemoryDb::setup_connection_pool(Settings::load().unwrap().node(), None)
386 .unwrap()
387 .conn()
388 .unwrap();
389 let (_, receipt) = test_utils::receipt::receipts();
390
391 let rows_inserted = diesel::insert_into(schema::receipts::table)
392 .values(&receipt)
393 .execute(&mut conn)
394 .unwrap();
395
396 assert_eq!(1, rows_inserted);
397 let inserted_receipt = schema::receipts::table.load::<Receipt>(&mut conn).unwrap();
398 assert_eq!(vec![receipt.clone()], inserted_receipt);
399 }
400
401 #[test]
402 fn receipt_to_json() {
403 let (_, receipt) = test_utils::receipt::receipts();
404 assert_eq!(
405 receipt.to_json_string().unwrap(),
406 format!(
407 r#"{{"cid":{{"/":"{}"}},"instruction":{{"/":"{}"}},"iss":null,"meta":null,"out":["ok",true],"prf":[],"ran":{{"/":"{}"}},"version":"{}"}}"#,
408 receipt.cid(),
409 receipt.instruction(),
410 receipt.ran(),
411 consts::INVOCATION_VERSION
412 )
413 );
414 }
415
416 #[test]
417 fn receipt_bytes_roundtrip() {
418 let (_, receipt) = test_utils::receipt::receipts();
419 let bytes: Vec<u8> = receipt.clone().try_into().unwrap();
420 let from_bytes = Receipt::try_from(bytes).unwrap();
421
422 assert_eq!(receipt, from_bytes);
423 }
424}