1#[cfg(not(target_os = "zkvm"))]
16use std::str::FromStr;
17use std::{borrow::Cow, ops::Not};
18
19#[cfg(not(target_os = "zkvm"))]
20use alloy::{
21 contract::Error as ContractErr,
22 primitives::{Signature, SignatureError},
23 signers::Signer,
24 sol_types::{Error as DecoderErr, SolInterface, SolStruct},
25 transports::TransportError,
26};
27use alloy_primitives::{
28 aliases::{U160, U32, U96},
29 Address, Bytes, FixedBytes, Keccak256, U256,
30};
31use alloy_sol_types::{eip712_domain, Eip712Domain, SolValue};
32use serde::{Deserialize, Serialize};
33#[cfg(not(target_os = "zkvm"))]
34use std::time::Duration;
35#[cfg(not(target_os = "zkvm"))]
36use thiserror::Error;
37#[cfg(not(target_os = "zkvm"))]
38use token::{
39 IHitPoints::{self, IHitPointsErrors},
40 IERC20::IERC20Errors,
41};
42use url::Url;
43
44use risc0_zkvm::{
45 sha::{Digest, Digestible},
46 MaybePruned, ReceiptClaim,
47};
48
49#[cfg(not(target_os = "zkvm"))]
50pub use risc0_ethereum_contracts::{encode_seal, selector::Selector, IRiscZeroSetVerifier};
51
52#[cfg(not(target_os = "zkvm"))]
53use crate::{input::GuestEnvBuilder, util::now_timestamp};
54
55#[cfg(not(target_os = "zkvm"))]
56const TXN_CONFIRM_TIMEOUT: Duration = Duration::from_secs(45);
57
58include!(concat!(env!("OUT_DIR"), "/boundless_market_generated.rs"));
62pub use boundless_market_contract::{
63 AssessorCallback, AssessorCommitment, AssessorJournal, AssessorReceipt, Callback, Fulfillment,
64 FulfillmentContext, FulfillmentDataImageIdAndJournal, FulfillmentDataType, IBoundlessMarket,
65 Input as RequestInput, InputType as RequestInputType, LockRequest, Offer,
66 Predicate as RequestPredicate, PredicateType, ProofRequest, RequestLock, Requirements,
67 Selector as AssessorSelector,
68};
69
70#[allow(missing_docs)]
71#[cfg(not(target_os = "zkvm"))]
72pub mod token {
73 use alloy::{
74 primitives::{Signature, B256},
75 signers::Signer,
76 sol_types::SolStruct,
77 };
78 use anyhow::Result;
79 use serde::Serialize;
80
81 alloy::sol!(
82 #![sol(rpc, all_derives)]
83 "src/contracts/artifacts/IHitPoints.sol"
84 );
85
86 alloy::sol! {
87 #[derive(Debug, Serialize)]
88 struct Permit {
89 address owner;
90 address spender;
91 uint256 value;
92 uint256 nonce;
93 uint256 deadline;
94 }
95 }
96
97 alloy::sol! {
98 #[derive(Debug)]
99 #[sol(rpc)]
100 interface IERC20 {
101 error ERC20InsufficientBalance(address sender, uint256 balance, uint256 needed);
102 error ERC20InvalidSender(address sender);
103 error ERC20InvalidReceiver(address receiver);
104 error ERC20InsufficientAllowance(address spender, uint256 allowance, uint256 needed);
105 error ERC20InvalidApprover(address approver);
106 error ERC20InvalidSpender(address spender);
107 function approve(address spender, uint256 value) external returns (bool);
108 function balanceOf(address account) external view returns (uint256);
109 function symbol() external view returns (string memory);
110 function decimals() external view returns (uint8);
111 }
112 }
113
114 alloy::sol! {
115 #[sol(rpc)]
116 interface IERC20Permit {
117 function nonces(address owner) external view returns (uint256);
118 function DOMAIN_SEPARATOR() external view returns (bytes32);
119 }
120 }
121
122 impl Permit {
123 pub async fn sign(
129 &self,
130 signer: &impl Signer,
131 domain_separator: B256,
132 ) -> Result<Signature> {
133 let struct_hash = self.eip712_hash_struct();
134 let prefix: &[u8] = &[0x19, 0x01];
135 let signing_bytes = prefix
136 .iter()
137 .chain(domain_separator.as_slice())
138 .chain(struct_hash.as_slice())
139 .cloned()
140 .collect::<Vec<u8>>();
141 let signing_hash = alloy::primitives::keccak256(signing_bytes);
142
143 Ok(signer.sign_hash(&signing_hash).await?)
144 }
145 }
146}
147
148#[derive(Default, Debug, PartialEq)]
150pub enum RequestStatus {
151 Expired,
153 Locked,
155 Fulfilled,
157 #[default]
163 Unknown,
164}
165
166#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
167pub struct EIP712DomainSaltless {
169 pub name: Cow<'static, str>,
171 pub version: Cow<'static, str>,
173 pub chain_id: u64,
175 pub verifying_contract: Address,
177}
178
179impl EIP712DomainSaltless {
180 pub fn alloy_struct(&self) -> Eip712Domain {
182 eip712_domain! {
183 name: self.name.clone(),
184 version: self.version.clone(),
185 chain_id: self.chain_id,
186 verifying_contract: self.verifying_contract,
187 }
188 }
189}
190
191#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
195#[non_exhaustive]
196pub struct RequestId {
197 pub addr: Address,
199 pub index: u32,
205 pub smart_contract_signed: bool,
208}
209
210impl RequestId {
211 pub fn new(addr: Address, index: u32) -> Self {
213 Self { addr, index, smart_contract_signed: false }
214 }
215
216 pub fn u256(addr: Address, index: u32) -> U256 {
218 Self::new(addr, index).into()
219 }
220
221 pub fn set_smart_contract_signed_flag(self) -> Self {
224 Self { addr: self.addr, index: self.index, smart_contract_signed: true }
225 }
226
227 pub fn from_lossy(value: U256) -> Self {
233 let mut addr_u256 = value >> U256::from(32);
234 addr_u256 &= (U256::from(1) << U256::from(160)) - U256::from(1); let addr = Address::from(addr_u256.to::<U160>());
236 Self {
237 addr,
238 index: (value & U32::MAX.to::<U256>()).to::<u32>(),
239 smart_contract_signed: (value & (U256::from(1) << 192)) != U256::ZERO,
240 }
241 }
242}
243
244impl TryFrom<U256> for RequestId {
245 type Error = RequestError;
246
247 fn try_from(value: U256) -> Result<Self, Self::Error> {
248 if value >> 193 != U256::ZERO {
251 return Err(RequestError::MalformedRequestId);
252 }
253 Ok(RequestId::from_lossy(value))
254 }
255}
256
257impl From<RequestId> for U256 {
258 fn from(value: RequestId) -> Self {
259 #[allow(clippy::unnecessary_fallible_conversions)] let addr = U160::try_from(value.addr).unwrap();
261 let smart_contract_signed_flag =
262 if value.smart_contract_signed { U256::from(1) } else { U256::ZERO };
263 (smart_contract_signed_flag << 192) | (U256::from(addr) << 32) | U256::from(value.index)
264 }
265}
266
267#[non_exhaustive]
268#[derive(thiserror::Error, Debug)]
269pub enum RequestError {
271 #[error("malformed request ID")]
273 MalformedRequestId,
274
275 #[error("request ID has client address of all zeroes")]
277 ClientAddrIsZero,
278
279 #[cfg(not(target_os = "zkvm"))]
281 #[error("signature error: {0}")]
282 SignatureError(#[from] alloy::signers::Error),
283
284 #[error("invalid signature: not normalized s-value")]
286 SignatureNonCanonicalError,
287
288 #[error("image URL must not be empty")]
290 EmptyImageUrl,
291
292 #[error("malformed image URL: {0}")]
294 MalformedImageUrl(#[from] url::ParseError),
295
296 #[error("image ID must not be ZERO")]
298 ImageIdIsZero,
299
300 #[error("offer timeout must be greater than 0")]
302 OfferTimeoutIsZero,
303
304 #[error("offer lock timeout must be greater than 0")]
306 OfferLockTimeoutIsZero,
307
308 #[error("offer ramp up period must be less than or equal to the lock timeout")]
310 OfferRampUpGreaterThanLockTimeout,
311
312 #[error("offer lock timeout must be less than or equal to the timeout")]
314 OfferLockTimeoutGreaterThanTimeout,
315
316 #[error("difference between timeout and lockTimeout much be less than 2^24")]
320 OfferTimeoutRangeTooLarge,
321
322 #[error("offer maxPrice must be greater than 0")]
324 OfferMaxPriceIsZero,
325
326 #[error("offer maxPrice must be greater than or equal to minPrice")]
328 OfferMaxPriceIsLessThanMin,
329
330 #[error("offer rampUpStart must be greater than 0")]
332 OfferRampUpStartIsZero,
333
334 #[error("missing requirements")]
336 MissingRequirements,
337
338 #[error("missing image URL")]
340 MissingImageUrl,
341
342 #[error("missing input")]
344 MissingInput,
345
346 #[error("missing offer")]
348 MissingOffer,
349
350 #[error("missing request ID")]
352 MissingRequestId,
353
354 #[error("request digest mismatch")]
356 DigestMismatch,
357
358 #[error("predicate data error: {0}")]
360 PredicateError(#[from] PredicateError),
361}
362
363#[cfg(not(target_os = "zkvm"))]
364impl From<SignatureError> for RequestError {
365 fn from(err: alloy::primitives::SignatureError) -> Self {
366 RequestError::SignatureError(err.into())
367 }
368}
369
370#[non_exhaustive]
371#[derive(thiserror::Error, Debug)]
372pub enum FulfillmentDataError {
374 #[error("malformed fulfillment data")]
376 Malformed,
377 #[error("invalid fulfillment data type")]
379 InvalidType,
380}
381
382impl ProofRequest {
383 pub fn new(
387 request_id: RequestId,
388 requirements: impl Into<Requirements>,
389 image_url: impl Into<String>,
390 input: impl Into<RequestInput>,
391 offer: impl Into<Offer>,
392 ) -> Self {
393 Self {
394 id: request_id.into(),
395 requirements: requirements.into(),
396 imageUrl: image_url.into(),
397 input: input.into(),
398 offer: offer.into(),
399 }
400 }
401
402 pub fn client_address(&self) -> Address {
404 RequestId::from_lossy(self.id).addr
405 }
406
407 pub fn expires_at(&self) -> u64 {
409 self.offer.rampUpStart + self.offer.timeout as u64
410 }
411
412 #[cfg(not(target_os = "zkvm"))]
417 pub fn is_expired(&self) -> bool {
418 self.expires_at() < now_timestamp()
419 }
420
421 pub fn lock_expires_at(&self) -> u64 {
423 self.offer.rampUpStart + self.offer.lockTimeout as u64
424 }
425
426 #[cfg(not(target_os = "zkvm"))]
431 pub fn is_lock_expired(&self) -> bool {
432 self.lock_expires_at() < now_timestamp()
433 }
434
435 pub fn is_smart_contract_signed(&self) -> bool {
438 RequestId::from_lossy(self.id).smart_contract_signed
439 }
440
441 pub fn validate(&self) -> Result<(), RequestError> {
449 if RequestId::from_lossy(self.id).addr == Address::ZERO {
450 return Err(RequestError::ClientAddrIsZero);
451 }
452 if self.imageUrl.is_empty() {
453 return Err(RequestError::EmptyImageUrl);
454 }
455 Url::parse(&self.imageUrl).map(|_| ())?;
456
457 Predicate::try_from(self.requirements.predicate.clone())?;
459
460 if self.offer.timeout == 0 {
461 return Err(RequestError::OfferTimeoutIsZero);
462 }
463 if self.offer.lockTimeout == 0 {
464 return Err(RequestError::OfferLockTimeoutIsZero);
465 }
466 if self.offer.rampUpPeriod > self.offer.lockTimeout {
467 return Err(RequestError::OfferRampUpGreaterThanLockTimeout);
468 }
469 if self.offer.lockTimeout > self.offer.timeout {
470 return Err(RequestError::OfferLockTimeoutGreaterThanTimeout);
471 }
472 if self.offer.timeout - self.offer.lockTimeout >= 1 << 24 {
473 return Err(RequestError::OfferTimeoutRangeTooLarge);
474 }
475 if self.offer.maxPrice == U256::ZERO {
476 return Err(RequestError::OfferMaxPriceIsZero);
477 }
478 if self.offer.maxPrice < self.offer.minPrice {
479 return Err(RequestError::OfferMaxPriceIsLessThanMin);
480 }
481 if self.offer.rampUpStart == 0 {
482 return Err(RequestError::OfferRampUpStartIsZero);
483 }
484
485 Ok(())
486 }
487}
488
489#[cfg(not(target_os = "zkvm"))]
490impl ProofRequest {
491 pub async fn sign_request(
494 &self,
495 signer: &impl Signer,
496 contract_addr: Address,
497 chain_id: u64,
498 ) -> Result<Signature, RequestError> {
499 let hash = self.signing_hash(contract_addr, chain_id)?;
500 Ok(signer.sign_hash(&hash).await?)
501 }
502
503 pub fn signing_hash(
505 &self,
506 contract_addr: Address,
507 chain_id: u64,
508 ) -> Result<FixedBytes<32>, RequestError> {
509 let domain = eip712_domain(contract_addr, chain_id);
510 let hash = self.eip712_signing_hash(&domain.alloy_struct());
511 Ok(hash)
512 }
513
514 pub fn verify_signature(
517 &self,
518 signature: &Bytes,
519 contract_addr: Address,
520 chain_id: u64,
521 ) -> Result<(), RequestError> {
522 let sig = Signature::try_from(signature.as_ref())?;
523 if sig.normalize_s().is_some() {
525 return Err(RequestError::SignatureNonCanonicalError);
526 }
527 let hash = self.signing_hash(contract_addr, chain_id)?;
528 let addr = sig.recover_address_from_prehash(&hash)?;
529 if addr == self.client_address() {
530 Ok(())
531 } else {
532 Err(SignatureError::FromBytes("Address mismatch").into())
533 }
534 }
535}
536
537impl Requirements {
538 pub fn new(predicate: impl Into<RequestPredicate>) -> Self {
540 Self {
541 predicate: predicate.into(),
542 callback: Callback::default(),
543 selector: UNSPECIFIED_SELECTOR,
544 }
545 }
546
547 pub fn with_predicate(self, predicate: impl Into<RequestPredicate>) -> Self {
549 Self { predicate: predicate.into(), ..self }
550 }
551
552 pub fn with_callback(self, callback: Callback) -> Self {
554 Self { callback, ..self }
555 }
556
557 pub fn with_selector(self, selector: FixedBytes<4>) -> Self {
559 Self { selector, ..self }
560 }
561
562 #[cfg(not(target_os = "zkvm"))]
568 pub fn with_groth16_proof(self) -> Self {
569 use crate::selector::SelectorExt;
570
571 match crate::util::is_dev_mode() {
572 true => Self { selector: FixedBytes::from(SelectorExt::FakeReceipt as u32), ..self },
573 false => {
574 Self { selector: FixedBytes::from(SelectorExt::groth16_latest() as u32), ..self }
575 }
576 }
577 }
578
579 #[cfg(not(target_os = "zkvm"))]
585 pub fn with_blake3_groth16_proof(self) -> Self {
586 use crate::selector::SelectorExt;
587
588 match crate::util::is_dev_mode() {
589 true => {
590 Self { selector: FixedBytes::from(SelectorExt::FakeBlake3Groth16 as u32), ..self }
591 }
592 false => Self {
593 selector: FixedBytes::from(SelectorExt::blake3_groth16_latest() as u32),
594 ..self
595 },
596 }
597 }
598}
599
600#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
602#[non_exhaustive]
603pub enum FulfillmentData {
604 None,
606 ImageIdAndJournal(Digest, Bytes),
608}
609
610impl FulfillmentData {
611 pub fn from_image_id_and_journal(
613 image_id: impl Into<Digest>,
614 journal: impl Into<Bytes>,
615 ) -> Self {
616 Self::ImageIdAndJournal(image_id.into(), journal.into())
617 }
618
619 pub fn fulfillment_type_and_data(&self) -> (FulfillmentDataType, Vec<u8>) {
621 match self {
622 Self::None => (FulfillmentDataType::None, Vec::new()),
623 Self::ImageIdAndJournal(image_id, journal) => (
624 FulfillmentDataType::ImageIdAndJournal,
625 FulfillmentDataImageIdAndJournal {
626 imageId: <[u8; 32]>::from(*image_id).into(),
627 journal: journal.clone(),
628 }
629 .abi_encode(),
630 ),
631 }
632 }
633
634 pub fn decode_with_type(
636 fill_type: FulfillmentDataType,
637 data: impl AsRef<[u8]>,
638 ) -> Result<Self, FulfillmentDataError> {
639 match fill_type {
640 FulfillmentDataType::None => {
641 if !data.as_ref().is_empty() {
642 return Err(FulfillmentDataError::Malformed);
643 }
644 Ok(Self::None)
645 }
646 FulfillmentDataType::ImageIdAndJournal => {
647 let FulfillmentDataImageIdAndJournal { imageId, journal } =
648 FulfillmentDataImageIdAndJournal::abi_decode(data.as_ref())
649 .map_err(|_| FulfillmentDataError::Malformed)?;
650 Ok(Self::ImageIdAndJournal(imageId.0.into(), journal))
651 }
652 FulfillmentDataType::__Invalid => Err(FulfillmentDataError::InvalidType),
653 }
654 }
655
656 pub fn fulfillment_data_digest(&self) -> Result<Digest, FulfillmentDataError> {
658 let mut hasher = Keccak256::new();
659 match self {
660 FulfillmentData::None => hasher.update([FulfillmentDataType::None as u8]),
661 FulfillmentData::ImageIdAndJournal(image_id, journal) => {
662 hasher.update([FulfillmentDataType::ImageIdAndJournal as u8]);
663 hasher.update(
664 FulfillmentDataImageIdAndJournal {
665 imageId: <[u8; 32]>::from(*image_id).into(),
666 journal: journal.clone(),
667 }
668 .abi_encode(),
669 );
670 }
671 }
672 Ok(hasher.finalize().0.into())
673 }
674
675 pub fn journal(&self) -> Option<&Bytes> {
677 match self {
678 FulfillmentData::None => None,
679 FulfillmentData::ImageIdAndJournal(_, journal) => Some(journal),
680 }
681 }
682
683 pub fn image_id(&self) -> Option<Digest> {
685 match self {
686 FulfillmentData::None => None,
687 FulfillmentData::ImageIdAndJournal(image_id, _) => Some(*image_id),
688 }
689 }
690}
691
692impl From<FulfillmentDataImageIdAndJournal> for FulfillmentData {
693 fn from(value: FulfillmentDataImageIdAndJournal) -> Self {
694 Self::ImageIdAndJournal(Digest::from_bytes(*value.imageId), value.journal)
695 }
696}
697
698impl Fulfillment {
699 pub fn data(&self) -> Result<FulfillmentData, FulfillmentDataError> {
701 FulfillmentData::decode_with_type(self.fulfillmentDataType, &self.fulfillmentData)
702 }
703}
704
705#[derive(thiserror::Error, Debug)]
706pub enum PredicateError {
708 #[error("malformed predicate data")]
710 DataMalformed,
711 #[error("invalid predicate type")]
713 InvalidType,
714}
715
716#[derive(Clone, Debug)]
717#[non_exhaustive]
718pub enum Predicate {
720 DigestMatch(Digest, Digest),
722 PrefixMatch(Digest, Bytes),
724 ClaimDigestMatch(Digest),
726}
727
728impl From<Predicate> for RequestPredicate {
729 fn from(value: Predicate) -> Self {
730 match value {
731 Predicate::DigestMatch(image_id, journal_digest) => Self {
732 predicateType: PredicateType::DigestMatch,
733 data: [image_id.as_bytes(), journal_digest.as_bytes()].concat().into(),
734 },
735 Predicate::PrefixMatch(image_id, journal_prefix) => Self {
736 predicateType: PredicateType::PrefixMatch,
737 data: [image_id.as_bytes(), journal_prefix.as_ref()].concat().into(),
738 },
739 Predicate::ClaimDigestMatch(claim_digest) => Self {
740 predicateType: PredicateType::ClaimDigestMatch,
741 data: claim_digest.as_bytes().to_vec().into(),
742 },
743 }
744 }
745}
746
747impl TryFrom<RequestPredicate> for Predicate {
748 type Error = PredicateError;
749
750 fn try_from(value: RequestPredicate) -> Result<Self, Self::Error> {
751 match value.predicateType {
752 PredicateType::DigestMatch => {
753 let (image_id, journal_digest) = value
754 .data
755 .as_ref()
756 .split_at_checked(32)
757 .ok_or(PredicateError::DataMalformed)?;
758 Ok(Predicate::DigestMatch(
759 Digest::try_from(image_id).map_err(|_| PredicateError::DataMalformed)?,
760 Digest::try_from(journal_digest).map_err(|_| PredicateError::DataMalformed)?,
761 ))
762 }
763 PredicateType::PrefixMatch => {
764 let (image_id, journal_prefix) = value
765 .data
766 .as_ref()
767 .split_at_checked(32)
768 .ok_or(PredicateError::DataMalformed)?;
769 Ok(Predicate::PrefixMatch(
770 Digest::try_from(image_id).map_err(|_| PredicateError::DataMalformed)?,
771 journal_prefix.to_vec().into(),
772 ))
773 }
774 PredicateType::ClaimDigestMatch => {
775 Ok(Predicate::ClaimDigestMatch(
777 Digest::try_from(value.data.as_ref())
778 .map_err(|_| PredicateError::DataMalformed)?,
779 ))
780 }
781 PredicateType::__Invalid => Err(PredicateError::InvalidType),
782 }
783 }
784}
785
786impl Predicate {
787 pub fn digest_match(image_id: impl Into<Digest>, journal_digest: impl Into<Digest>) -> Self {
790 Self::DigestMatch(image_id.into(), journal_digest.into())
791 }
792 pub fn prefix_match(image_id: impl Into<Digest>, journal_prefix: impl Into<Bytes>) -> Self {
795 Self::PrefixMatch(image_id.into(), journal_prefix.into())
796 }
797 pub fn claim_digest_match(claim_digest: impl Into<Digest>) -> Self {
800 Self::ClaimDigestMatch(claim_digest.into())
801 }
802
803 pub fn image_id(&self) -> Option<Digest> {
805 match self {
806 Predicate::DigestMatch(image_id, ..) => Some(*image_id),
807 Predicate::PrefixMatch(image_id, ..) => Some(*image_id),
808 Predicate::ClaimDigestMatch(..) => None,
809 }
810 }
811
812 pub fn eval(&self, fulfillment_data: &FulfillmentData) -> Option<Digest> {
815 let claim_digest_data = match fulfillment_data {
816 FulfillmentData::None => None,
817 FulfillmentData::ImageIdAndJournal(image_id, journal) => {
818 Some(ReceiptClaim::ok(*image_id, journal.to_vec()).digest())
819 }
820 };
821 let claim_digest_predicate = match self {
822 Predicate::DigestMatch(image_id, journal) => {
823 let FulfillmentData::ImageIdAndJournal(_, _) = &fulfillment_data else {
826 return None;
827 };
828 Some(ReceiptClaim::ok(*image_id, MaybePruned::Pruned(*journal)).digest())
829 }
830 Predicate::PrefixMatch(image_id, prefix) => {
831 let FulfillmentData::ImageIdAndJournal(fill_image_id, fill_journal) =
833 &fulfillment_data
834 else {
835 return None;
836 };
837 let matches =
838 fill_image_id == image_id && fill_journal.starts_with(prefix.as_ref());
839 if !matches {
840 return None;
841 }
842 None
843 }
844 Predicate::ClaimDigestMatch(claim_digest) => Some(*claim_digest),
845 };
846 if let (Some(claim_digest_predicate), Some(claim_digest_data)) =
847 (claim_digest_predicate, claim_digest_data)
848 {
849 if claim_digest_predicate != claim_digest_data {
850 return None;
851 }
852 }
853 claim_digest_data.or(claim_digest_predicate)
854 }
855}
856
857impl Callback {
858 pub const NONE: Self = Self { addr: Address::ZERO, gasLimit: U96::ZERO };
860
861 pub fn with_addr(self, addr: impl Into<Address>) -> Self {
863 Self { addr: addr.into(), ..self }
864 }
865
866 pub fn with_gas_limit(self, gas_limit: u64) -> Self {
868 Self { gasLimit: U96::from(gas_limit), ..self }
869 }
870
871 pub fn is_none(&self) -> bool {
875 self.addr == Address::ZERO
876 }
877
878 pub fn into_option(self) -> Option<Self> {
880 self.is_none().not().then_some(self)
881 }
882
883 pub fn as_option(&self) -> Option<&Self> {
885 self.is_none().not().then_some(self)
886 }
887
888 pub fn from_option(opt: Option<Self>) -> Self {
890 opt.unwrap_or(Self::NONE)
891 }
892}
893
894impl RequestInput {
895 #[cfg(not(target_os = "zkvm"))]
897 pub fn builder() -> GuestEnvBuilder {
898 GuestEnvBuilder::new()
899 }
900
901 pub fn inline(data: impl Into<Bytes>) -> Self {
915 Self { inputType: RequestInputType::Inline, data: data.into() }
916 }
917
918 pub fn url(url: impl Into<String>) -> Self {
920 Self { inputType: RequestInputType::Url, data: url.into().as_bytes().to_vec().into() }
921 }
922}
923
924impl From<Url> for RequestInput {
925 fn from(value: Url) -> Self {
927 Self::url(value)
928 }
929}
930
931impl Offer {
932 pub fn with_min_price(self, min_price: U256) -> Self {
934 Self { minPrice: min_price, ..self }
935 }
936
937 pub fn with_max_price(self, max_price: U256) -> Self {
939 Self { maxPrice: max_price, ..self }
940 }
941
942 pub fn with_lock_collateral(self, lock_collateral: U256) -> Self {
944 Self { lockCollateral: lock_collateral, ..self }
945 }
946
947 pub fn with_ramp_up_start(self, ramp_up_start: u64) -> Self {
949 Self { rampUpStart: ramp_up_start, ..self }
950 }
951
952 pub fn with_timeout(self, timeout: u32) -> Self {
954 Self { timeout, ..self }
955 }
956
957 pub fn with_lock_timeout(self, lock_timeout: u32) -> Self {
959 Self { lockTimeout: lock_timeout, ..self }
960 }
961
962 pub fn with_ramp_up_period(self, ramp_up_period: u32) -> Self {
965 Self { rampUpPeriod: ramp_up_period, ..self }
966 }
967
968 pub fn with_min_price_per_mcycle(self, mcycle_price: U256, mcycle: u64) -> Self {
970 let min_price = mcycle_price * U256::from(mcycle);
971 Self { minPrice: min_price, ..self }
972 }
973
974 pub fn with_max_price_per_mcycle(self, mcycle_price: U256, mcycle: u64) -> Self {
976 let max_price = mcycle_price * U256::from(mcycle);
977 Self { maxPrice: max_price, ..self }
978 }
979
980 pub fn with_lock_collateral_per_mcycle(self, mcycle_price: U256, mcycle: u64) -> Self {
982 let lock_collateral = mcycle_price * U256::from(mcycle);
983 Self { lockCollateral: lock_collateral, ..self }
984 }
985
986 #[cfg(not(target_os = "zkvm"))]
991 pub fn required_khz_performance(&self, cycle_count: u64) -> f64 {
992 if self.lockTimeout == 0 {
993 tracing::warn!(
994 "lockTimeout is zero, cannot calculate required performance. Returning infinity."
995 );
996 return f64::INFINITY;
997 }
998 let frequency = cycle_count as f64 / (self.lockTimeout as f64);
999 frequency / 1_000.0
1001 }
1002
1003 #[cfg(not(target_os = "zkvm"))]
1009 pub fn required_khz_performance_secondary_prover(&self, cycle_count: u64) -> f64 {
1010 let secondary_window = self.timeout.saturating_sub(self.lockTimeout);
1011 if secondary_window == 0 {
1012 tracing::warn!(
1013 "No secondary window available (timeout <= lockTimeout). Secondary prover cannot fulfill request. Returning infinity."
1014 );
1015 return f64::INFINITY;
1016 }
1017 let frequency = cycle_count as f64 / (secondary_window as f64);
1018 frequency / 1_000.0
1020 }
1021}
1022
1023#[cfg(not(target_os = "zkvm"))]
1024use IBoundlessMarket::IBoundlessMarketErrors;
1025#[cfg(not(target_os = "zkvm"))]
1026use IRiscZeroSetVerifier::IRiscZeroSetVerifierErrors;
1027
1028#[cfg(not(target_os = "zkvm"))]
1029pub mod boundless_market;
1031#[cfg(not(target_os = "zkvm"))]
1032pub mod hit_points;
1034
1035pub mod pricing;
1039
1040#[cfg(not(target_os = "zkvm"))]
1041#[derive(Error, Debug)]
1042pub enum TxnErr {
1044 #[error("SetVerifier error: {0:?}")]
1046 SetVerifierErr(IRiscZeroSetVerifierErrors),
1047
1048 #[error("BoundlessMarket Err: {0:?}")]
1050 BoundlessMarketErr(IBoundlessMarket::IBoundlessMarketErrors),
1051
1052 #[error("HitPoints Err: {0:?}")]
1054 HitPointsErr(IHitPoints::IHitPointsErrors),
1055
1056 #[error("IERC20 Err: {0:?}")]
1058 ERC20Err(token::IERC20::IERC20Errors),
1059
1060 #[error("decoding err, missing data, code: {0} msg: {1}")]
1062 MissingData(i64, String),
1063
1064 #[error("decoding err: bytes decoding")]
1066 BytesDecode,
1067
1068 #[error("contract error: {0}")]
1070 ContractErr(ContractErr),
1071
1072 #[error("abi decoder error: {0} - {1}")]
1074 DecodeErr(DecoderErr, Bytes),
1075}
1076
1077#[cfg(not(target_os = "zkvm"))]
1079impl From<ContractErr> for TxnErr {
1080 fn from(err: ContractErr) -> Self {
1081 match &err {
1082 ContractErr::TransportError(TransportError::ErrorResp(ts_err)) => {
1083 let Some(data) = &ts_err.data else {
1084 return TxnErr::MissingData(ts_err.code, ts_err.message.to_string());
1085 };
1086
1087 let data = data.get().trim_matches('"');
1088
1089 let Ok(data) = Bytes::from_str(data) else {
1090 return Self::BytesDecode;
1091 };
1092
1093 if let Ok(decoded_error) = IBoundlessMarketErrors::abi_decode(&data) {
1095 Self::BoundlessMarketErr(decoded_error)
1096 } else if let Ok(decoded_error) = IHitPointsErrors::abi_decode(&data) {
1097 Self::HitPointsErr(decoded_error)
1098 } else if let Ok(decoded_error) = IRiscZeroSetVerifierErrors::abi_decode(&data) {
1099 Self::SetVerifierErr(decoded_error)
1100 } else if let Ok(decoded_error) = IERC20Errors::abi_decode(&data) {
1101 Self::ERC20Err(decoded_error)
1102 } else {
1103 Self::ContractErr(err)
1104 }
1105 }
1106 _ => Self::ContractErr(err),
1107 }
1108 }
1109}
1110
1111#[cfg(not(target_os = "zkvm"))]
1112fn decode_contract_err<T: SolInterface>(err: ContractErr) -> Result<T, TxnErr> {
1113 match err {
1114 ContractErr::TransportError(TransportError::ErrorResp(ts_err)) => {
1115 let Some(data) = ts_err.data else {
1116 return Err(TxnErr::MissingData(ts_err.code, ts_err.message.to_string()));
1117 };
1118
1119 let data = data.get().trim_matches('"');
1120
1121 let Ok(data) = Bytes::from_str(data) else {
1122 return Err(TxnErr::BytesDecode);
1123 };
1124
1125 let decoded_error = match T::abi_decode(&data) {
1126 Ok(res) => res,
1127 Err(err) => {
1128 return Err(TxnErr::DecodeErr(err, data));
1129 }
1130 };
1131
1132 Ok(decoded_error)
1133 }
1134 _ => Err(TxnErr::ContractErr(err)),
1135 }
1136}
1137
1138#[cfg(not(target_os = "zkvm"))]
1139impl IHitPointsErrors {
1140 pub(crate) fn decode_error(err: ContractErr) -> TxnErr {
1141 match decode_contract_err(err) {
1142 Ok(res) => TxnErr::HitPointsErr(res),
1143 Err(decode_err) => decode_err,
1144 }
1145 }
1146}
1147
1148#[cfg(not(target_os = "zkvm"))]
1149pub fn eip712_domain(addr: Address, chain_id: u64) -> EIP712DomainSaltless {
1151 EIP712DomainSaltless {
1152 name: "IBoundlessMarket".into(),
1153 version: "1".into(),
1154 chain_id,
1155 verifying_contract: addr,
1156 }
1157}
1158
1159pub const UNSPECIFIED_SELECTOR: FixedBytes<4> = FixedBytes::<4>([0; 4]);
1161
1162#[cfg(feature = "test-utils")]
1163#[allow(missing_docs)]
1164pub mod bytecode;
1165
1166#[cfg(test)]
1167mod tests {
1168 use super::*;
1169 use alloy::signers::local::PrivateKeySigner;
1170
1171 async fn create_order(
1172 signer: &impl Signer,
1173 signer_addr: Address,
1174 order_id: u32,
1175 contract_addr: Address,
1176 chain_id: u64,
1177 ) -> (ProofRequest, [u8; 65]) {
1178 let request_id = RequestId::u256(signer_addr, order_id);
1179
1180 let req = ProofRequest {
1181 id: request_id,
1182 requirements: Requirements::new(Predicate::prefix_match(
1183 Digest::ZERO,
1184 Bytes::default(),
1185 )),
1186 imageUrl: "https://dev.null".to_string(),
1187 input: RequestInput::builder().build_inline().unwrap(),
1188 offer: Offer {
1189 minPrice: U256::from(0),
1190 maxPrice: U256::from(1),
1191 rampUpStart: 0,
1192 timeout: 1000,
1193 rampUpPeriod: 1,
1194 lockTimeout: 1000,
1195 lockCollateral: U256::from(0),
1196 },
1197 };
1198
1199 let client_sig = req.sign_request(signer, contract_addr, chain_id).await.unwrap();
1200
1201 (req, client_sig.as_bytes())
1202 }
1203
1204 #[tokio::test]
1205 async fn validate_sig() {
1206 let signer: PrivateKeySigner =
1207 "6f142508b4eea641e33cb2a0161221105086a84584c74245ca463a49effea30b".parse().unwrap();
1208 let order_id: u32 = 1;
1209 let contract_addr = Address::ZERO;
1210 let chain_id = 1;
1211 let signer_addr = signer.address();
1212
1213 let (req, client_sig) =
1214 create_order(&signer, signer_addr, order_id, contract_addr, chain_id).await;
1215
1216 req.verify_signature(&Bytes::from(client_sig), contract_addr, chain_id).unwrap();
1217 }
1218
1219 #[tokio::test]
1220 #[should_panic(expected = "SignatureError")]
1221 async fn invalid_sig() {
1222 let signer: PrivateKeySigner =
1223 "6f142508b4eea641e33cb2a0161221105086a84584c74245ca463a49effea30b".parse().unwrap();
1224 let order_id: u32 = 1;
1225 let contract_addr = Address::ZERO;
1226 let chain_id = 1;
1227 let signer_addr = signer.address();
1228
1229 let (req, mut client_sig) =
1230 create_order(&signer, signer_addr, order_id, contract_addr, chain_id).await;
1231
1232 client_sig[0] = 1;
1233 req.verify_signature(&Bytes::from(client_sig), contract_addr, chain_id).unwrap();
1234 }
1235
1236 #[tokio::test]
1237 async fn test_request_id() {
1238 let raw_id1 =
1240 U256::from_str("3130239009558586413752262552917257075388277690201777635428").unwrap();
1241 let request_id1 = RequestId::from_lossy(raw_id1);
1242
1243 let client1 = request_id1.addr;
1244 let idx1 = request_id1.index;
1245 let is_smart_contract1 = request_id1.smart_contract_signed;
1246
1247 assert_eq!(
1248 client1,
1249 Address::from_str("0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496").unwrap()
1250 );
1251 assert_eq!(idx1, 100);
1252 assert!(!is_smart_contract1);
1253
1254 let raw_id2 =
1256 U256::from_str("9407340744945267177588051976124923491490633134665812148266").unwrap();
1257 let request_id2 = RequestId::from_lossy(raw_id2);
1258
1259 let client2 = request_id2.addr;
1260 let idx2 = request_id2.index;
1261 let is_smart_contract2 = request_id2.smart_contract_signed;
1262
1263 assert_eq!(
1264 client2,
1265 Address::from_str("0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496").unwrap()
1266 );
1267 assert_eq!(idx2, 42);
1268 assert!(is_smart_contract2);
1269
1270 let request_id1_u256: U256 = request_id1.into();
1272 let request_id2_u256: U256 = request_id2.into();
1273 assert_eq!(request_id1_u256, raw_id1);
1274 assert_eq!(request_id2_u256, raw_id2);
1275 }
1276}