Skip to content
This repository has been archived by the owner on Dec 12, 2024. It is now read-only.

Commit

Permalink
Bring resource and offering data structures up to latest spec (#56)
Browse files Browse the repository at this point in the history
  • Loading branch information
KendallWeihe authored May 3, 2024
1 parent d4097d7 commit c5c234e
Show file tree
Hide file tree
Showing 3 changed files with 118 additions and 81 deletions.
6 changes: 4 additions & 2 deletions crates/protocol/src/resources/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ pub struct ResourceMetadata {
/// ISO 8601 timestamp
pub created_at: DateTime<Utc>,
/// ISO 8601 timestamp
pub updated_at: DateTime<Utc>,
pub updated_at: Option<DateTime<Utc>>,
/// Version of the protocol in use (x.x format). The protocol version must remain consistent across messages in a given exchange. Messages sharing the same exchangeId MUST also have the same protocol version.
pub protocol: String,
}

/// A struct representing the structure and common functionality available to all Resources.
Expand All @@ -37,7 +39,7 @@ pub struct Resource<T> {
/// The actual Resource content
pub data: T,
/// The signature that verifies the authenticity and integrity of the Resource
pub signature: Option<String>,
pub signature: String,
}

/// Errors that can occur when working with [`Resource`]s.
Expand Down
157 changes: 100 additions & 57 deletions crates/protocol/src/resources/offering.rs
Original file line number Diff line number Diff line change
@@ -1,94 +1,136 @@
use crate::resources::{Resource, ResourceError, ResourceKind, ResourceMetadata};
use chrono::Utc;
use credentials::pex::v2::PresentationDefinition;
use jsonschema::{Draft, JSONSchema};
use serde::{Deserialize, Serialize};
use serde_json::Value as JsonValue;
use serde_with::skip_serializing_none;

/// Struct that interacts with an [`Offering`] [`Resource`]
pub struct Offering;

/// Struct for passing parameters to [`Offering::create`]
#[derive(Debug, Default)]
pub struct CreateOptions {
pub from: String,
pub protocol: Option<String>,
pub data: OfferingData,
}

impl Offering {
pub fn create(
from: String,
data: OfferingData,
) -> Result<Resource<OfferingData>, ResourceError> {
pub fn create(options: CreateOptions) -> Result<Resource<OfferingData>, ResourceError> {
let metadata = ResourceMetadata {
id: ResourceKind::Offering.typesafe_id()?,
kind: ResourceKind::Offering,
from,
from: options.from,
created_at: Utc::now(),
updated_at: Utc::now(),
updated_at: Some(Utc::now()),
protocol: match options.protocol {
Some(p) => p,
None => "1.0".to_string(),
},
};

// todo implement signing https://github.com/TBD54566975/tbdex-rs/issues/27
let signature = "todo a valid signature".to_string();

Ok(Resource {
metadata,
data,
signature: None,
data: options.data,
signature,
})
}
}

/// A struct representing the data contained within the [`Resource`] for an [`Offering`].
/// Struct the data contained within the [`Resource`] for an [`Offering`].
///
/// See [Offering](https://github.com/TBD54566975/tbdex/tree/main/specs/protocol#offering) for more
/// information.
#[derive(Debug, Deserialize, PartialEq, Serialize)]
#[derive(Debug, Deserialize, PartialEq, Serialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct OfferingData {
/// Brief description of what is being offered.
pub description: String,
/// Number of payout units Alice would get for 1 payin unit
pub payout_units_per_payin_unit: String,
/// Details about the currency that the PFI is accepting as payment.
pub payin_currency: CurrencyDetails,
/// Details about the currency that the PFI is selling.
pub payout_currency: CurrencyDetails,
/// A list of payment methods the counterparty (Alice) can choose to send payment to the PFI
/// from in order to qualify for this offering.
pub payin_methods: Vec<PaymentMethod>,
/// A list of payment methods the counterparty (Alice) can choose to receive payment from the
/// PFI in order to qualify for this offering.
pub payout_methods: Vec<PaymentMethod>,
/// Articulates the claim(s) required when submitting an RFQ for this offering.
/// Details and options associated to the payin currency
pub payin: PayinDetails,
/// Details and options associated to the payout currency
pub payout: PayoutDetails,
/// Claim(s) required when submitting an RFQ for this offering.
pub required_claims: PresentationDefinition,
}

#[derive(Debug, Default, Deserialize, PartialEq, Serialize)]
#[skip_serializing_none]
/// Struct for [Offering's PayinDetails](https://github.com/TBD54566975/tbdex/tree/main/specs/protocol#payindetails)
#[derive(Debug, Deserialize, PartialEq, Serialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct CurrencyDetails {
/// ISO 3166 currency code string
pub struct PayinDetails {
/// ISO 4217 currency code string
pub currency_code: String,
/// Minimum amount of currency that the offer is valid for
pub min_subunits: Option<String>,
pub min: Option<String>,
/// Maximum amount of currency that the offer is valid for
pub max_subunits: Option<String>,
pub max: Option<String>,
/// A list of payment methods to select from
pub methods: Vec<PayinMethod>,
}

#[derive(Debug, Default, Deserialize, PartialEq, Serialize)]
#[skip_serializing_none]
/// Struct for [Offering's PayinMethod](https://github.com/TBD54566975/tbdex/tree/main/specs/protocol#payinmethod)
#[derive(Debug, Deserialize, PartialEq, Serialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct PaymentMethod {
/// Type of payment method (i.e. `DEBIT_CARD`, `BITCOIN_ADDRESS`, `SQUARE_PAY`)
pub struct PayinMethod {
/// Unique string identifying a single kind of payment method i.e. (i.e. DEBIT_CARD, BITCOIN_ADDRESS, SQUARE_PAY)
pub kind: String,
/// A JSON Schema containing the fields that need to be collected in order to use this
/// payment method
/// Payment Method name. Expected to be rendered on screen.
pub name: Option<String>,
/// Blurb containing helpful information about the payment method. Expected to be rendered on screen. e.g. "segwit addresses only"
pub description: Option<String>,
/// The category for which the given method belongs to
pub group: Option<String>,
/// A JSON Schema containing the fields that need to be collected in the RFQ's selected payment methods in order to use this payment method.
pub required_payment_details: Option<JsonValue>,
/// The fee expressed in the currency's sub units to make use of this payment method
pub fee_subunits: Option<String>,
/// Fee charged to use this payment method. absence of this field implies that there is no additional fee associated to the respective payment method
pub fee: Option<String>,
/// Minimum amount required to use this payment method.
pub min: Option<String>,
/// Maximum amount allowed when using this payment method.
pub max: Option<String>,
}

impl PaymentMethod {
pub fn required_payment_details_schema(&self) -> Option<JSONSchema> {
self.required_payment_details.as_ref().and_then(|json| {
JSONSchema::options()
.with_draft(Draft::Draft7)
.compile(json)
.ok()
})
}
/// Struct for [Offering's PayoutDetails](https://github.com/TBD54566975/tbdex/tree/main/specs/protocol#payoutdetails)
#[derive(Debug, Deserialize, PartialEq, Serialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct PayoutDetails {
/// ISO 4217 currency code string
pub currency_code: String,
/// Minimum amount of currency that the offer is valid for
pub min: Option<String>,
/// Maximum amount of currency that the offer is valid for
pub max: Option<String>,
/// A list of payment methods to select from
pub methods: Vec<PayoutMethod>,
}

/// Struct for [Offering's PayinMethod](https://github.com/TBD54566975/tbdex/tree/main/specs/protocol#payinmethod)
#[derive(Debug, Deserialize, PartialEq, Serialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct PayoutMethod {
/// Unique string identifying a single kind of payment method i.e. (i.e. DEBIT_CARD, BITCOIN_ADDRESS, SQUARE_PAY)
pub kind: String,
/// Estimated time taken to settle an order, expressed in seconds
pub estimated_settlement_time: u64,
/// Payment Method name. Expected to be rendered on screen.
pub name: Option<String>,
/// Blurb containing helpful information about the payment method. Expected to be rendered on screen. e.g. "segwit addresses only"
pub description: Option<String>,
/// The category for which the given method belongs to
pub group: Option<String>,
/// A JSON Schema containing the fields that need to be collected in the RFQ's selected payment methods in order to use this payment method.
pub required_payment_details: Option<JsonValue>,
/// Fee charged to use this payment method. absence of this field implies that there is no additional fee associated to the respective payment method
pub fee: Option<String>,
/// Minimum amount required to use this payment method.
pub min: Option<String>,
/// Maximum amount allowed when using this payment method.
pub max: Option<String>,
}

#[cfg(test)]
Expand All @@ -98,28 +140,29 @@ mod tests {

#[test]
fn can_create() {
let offering = Offering::create(
"did:example:1234".to_string(),
OfferingData {
let offering = Offering::create(CreateOptions {
from: "did:example:1234".to_string(),
data: OfferingData {
description: "my fake offering".to_string(),
payout_units_per_payin_unit: "1".to_string(),
payin_currency: CurrencyDetails {
payout_units_per_payin_unit: "2".to_string(),
payin: PayinDetails {
currency_code: "USD".to_string(),
..Default::default()
},
payout_currency: CurrencyDetails {
currency_code: "USD".to_string(),
payout: PayoutDetails {
currency_code: "BTC".to_string(),
..Default::default()
},
payin_methods: vec![],
payout_methods: vec![],
required_claims: PresentationDefinition::default(),
},
)
..Default::default()
})
.expect("failed to create offering");

assert_eq!(offering.data.description, "my fake offering");
assert_eq!(offering.metadata.id.type_prefix(), "offering");
assert_eq!(offering.metadata.from, "did:example:1234".to_string());
assert_eq!(offering.metadata.protocol, "1.0".to_string());
assert_eq!(offering.data.description, "my fake offering");
}

#[test]
Expand Down
36 changes: 14 additions & 22 deletions crates/protocol/src/test_data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ use crate::messages::quote::{
PaymentInstruction, PaymentInstructions, Quote, QuoteData, QuoteDetails,
};
use crate::messages::Message;
use crate::resources::offering::{CurrencyDetails, Offering, OfferingData, PaymentMethod};
use crate::resources::offering::{
CreateOptions, Offering, OfferingData, PayinDetails, PayoutDetails,
};
use crate::resources::Resource;
use chrono::Utc;
use credentials::pex::v2::{Constraints, Field, InputDescriptor, PresentationDefinition};
Expand All @@ -16,33 +18,23 @@ pub struct TestData;
#[cfg(test)]
impl TestData {
pub fn get_offering(from: String) -> Resource<OfferingData> {
Offering::create(
Offering::create(CreateOptions {
from,
OfferingData {
description: "A sample offering".to_string(),
payout_units_per_payin_unit: "1".to_string(),
payin_currency: CurrencyDetails {
currency_code: "AUD".to_string(),
min_subunits: Some("1".to_string()),
max_subunits: Some("10000".to_string()),
},
payout_currency: CurrencyDetails {
currency_code: "USDC".to_string(),
data: OfferingData {
description: "my fake offering".to_string(),
payout_units_per_payin_unit: "2".to_string(),
payin: PayinDetails {
currency_code: "USD".to_string(),
..Default::default()
},
payin_methods: vec![PaymentMethod {
kind: "BTC_ADDRESS".to_string(),
required_payment_details: Some(TestData::required_payment_details_schema()),
..Default::default()
}],
payout_methods: vec![PaymentMethod {
kind: "MOMO".to_string(),
required_payment_details: Some(TestData::required_payment_details_schema()),
payout: PayoutDetails {
currency_code: "BTC".to_string(),
..Default::default()
}],
},
required_claims: TestData::get_presentation_definition(),
},
)
..Default::default()
})
.expect("failed to create offering")
}

Expand Down

0 comments on commit c5c234e

Please sign in to comment.