Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion contextvm-ffi/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@
__pycache__/
*.py[cod]

# C test binary built by c-tests/Makefile
# C test binary + debug bundle built by c-tests/Makefile
c-tests/test_ffi
c-tests/test_ffi.dSYM/
1 change: 1 addition & 0 deletions contextvm-ffi/c-tests/test_ffi.c
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ TEST(constant_values) {
ASSERT(CVM_VALIDATION == 5);
ASSERT(CVM_UNAUTHORIZED == 6);
ASSERT(CVM_SERIALIZATION == 7);
ASSERT(CVM_PAYMENT == 8);
ASSERT(CVM_OTHER == 99);

ASSERT(CVM_ENCRYPTION_DISABLED == 2);
Expand Down
1 change: 1 addition & 0 deletions contextvm-ffi/headers/contextvm.h
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ typedef enum {
CVM_VALIDATION = 5,
CVM_UNAUTHORIZED = 6,
CVM_SERIALIZATION = 7,
CVM_PAYMENT = 8,
CVM_OTHER = 99,
} CvmErrorCode;

Expand Down
7 changes: 7 additions & 0 deletions contextvm-ffi/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ pub enum ErrorCode {
Unauthorized = 6,
/// Serialization/deserialization error.
Serialization = 7,
/// Payment (CEP-8) error.
Payment = 8,
/// Generic / unknown error.
Other = 99,
}
Expand All @@ -43,6 +45,11 @@ impl From<&contextvm_sdk::Error> for ErrorCode {
// transport-layer, so they surface as CVM_TRANSPORT. The C header
// exposes no dedicated code; mirrors `OversizedTransfer`.
contextvm_sdk::Error::OpenStream(_) => ErrorCode::Transport,
// CEP-8 payment failures get a dedicated FFI code so foreign callers can
// branch on "payment required/failed" rather than a generic error. The
// wildcard on the inner PaymentError keeps every later payment error off
// this map.
contextvm_sdk::Error::Payment(_) => ErrorCode::Payment,
contextvm_sdk::Error::Other(_) => ErrorCode::Other,
}
}
Expand Down
17 changes: 17 additions & 0 deletions src/core/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,18 @@ pub mod tags {

/// Support CEP-41 open-ended streaming via notifications/progress framing
pub const SUPPORT_OPEN_STREAM: &str = "support_open_stream";

/// CEP-8 payment method identifier advertisement/negotiation tag.
pub const PMI: &str = "pmi";

/// CEP-8 session payment-interaction negotiation tag.
pub const PAYMENT_INTERACTION: &str = "payment_interaction";

/// CEP-8 optional bearer-asset direct-payment tag (type/name only; never produced by this SDK).
pub const DIRECT_PAYMENT: &str = "direct_payment";

/// CEP-8 optional bearer-asset change tag (type/name only; never produced by this SDK).
pub const CHANGE: &str = "change";
}

/// Maximum message size (1MB)
Expand Down Expand Up @@ -181,6 +193,11 @@ mod tests {
tags::SUPPORT_OVERSIZED_TRANSFER,
"support_oversized_transfer"
);
assert_eq!(tags::SUPPORT_OPEN_STREAM, "support_open_stream");
assert_eq!(tags::PMI, "pmi");
assert_eq!(tags::PAYMENT_INTERACTION, "payment_interaction");
assert_eq!(tags::DIRECT_PAYMENT, "direct_payment");
assert_eq!(tags::CHANGE, "change");
}

#[test]
Expand Down
4 changes: 4 additions & 0 deletions src/core/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ pub enum Error {
#[error("Open stream error: {0}")]
OpenStream(#[from] crate::transport::open_stream::OpenStreamError),

/// CEP-8 payment error (pricing, issuance, verification, or settlement).
#[error("Payment error: {0}")]
Payment(#[from] crate::payments::PaymentError),

/// Generic error
#[error("{0}")]
Other(String),
Expand Down
16 changes: 16 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ pub mod discovery;
pub mod encryption;
/// Gateway bridging a local MCP server to Nostr
pub mod gateway;
/// CEP-8 capability pricing and payment primitives
pub mod payments;
/// Proxy connecting to a remote MCP server via Nostr
pub mod proxy;
/// Nostr relay pool management
Expand Down Expand Up @@ -88,6 +90,20 @@ pub use transport::server::{
SessionSnapshot, SessionStore,
};

// ── Payments (CEP-8) ─────────────────────────────────────────────────
#[cfg(feature = "test-utils")]
pub use payments::{
FakePaymentHandler, FakePaymentHandlerOptions, FakePaymentProcessor,
FakePaymentProcessorOptions,
};
pub use payments::{
Meta, PaymentAcceptedParams, PaymentError, PaymentHandler, PaymentHandlerRequest,
PaymentInteractionPolicy, PaymentOption, PaymentPendingErrorData, PaymentProcessor,
PaymentProcessorCreateParams, PaymentProcessorVerifyParams, PaymentRejectedParams,
PaymentRequiredErrorData, PaymentRequiredParams, PricedCapability, ResolvePrice,
ResolvePriceParams, ResolvePriceResult, VerifyOutcome,
};

// ── rmcp re-export ──────────────────────────────────────────────────
#[cfg(feature = "rmcp")]
pub use rmcp;
Expand Down
57 changes: 57 additions & 0 deletions src/payments/constants.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
//! CEP-8 payment protocol constants (methods, JSON-RPC error codes, PMIs, TTLs).

/// `notifications/payment_required`: server requests payment (transparent lifecycle).
pub const PAYMENT_REQUIRED_METHOD: &str = "notifications/payment_required";
/// `notifications/payment_accepted`: server observed settlement.
pub const PAYMENT_ACCEPTED_METHOD: &str = "notifications/payment_accepted";
/// `notifications/payment_rejected`: server refused or rejected payment.
pub const PAYMENT_REJECTED_METHOD: &str = "notifications/payment_rejected";

/// Explicit-gating JSON-RPC error: payment required (`error.data` = [`PaymentRequiredErrorData`](crate::payments::PaymentRequiredErrorData)).
pub const PAYMENT_REQUIRED_ERROR_CODE: i64 = -32042;
/// Explicit-gating JSON-RPC error: payment pending (`error.data` = [`PaymentPendingErrorData`](crate::payments::PaymentPendingErrorData)).
pub const PAYMENT_PENDING_ERROR_CODE: i64 = -32043;
/// Unsupported `payment_interaction` negotiation: reuses JSON-RPC Invalid Params per CEP-8.
pub const UNSUPPORTED_PAYMENT_INTERACTION_ERROR_CODE: i64 = -32602;

/// The only Phase-A payment method identifier.
pub const PMI_BITCOIN_LIGHTNING_BOLT11: &str = "bitcoin-lightning-bolt11";

/// Default payment TTL when `payment_required` carries no `ttl` (ms). Mirrors the server default.
pub const DEFAULT_PAYMENT_TTL_MS: u64 = 300_000;
/// Default synthetic-progress heartbeat interval (ms). Half the 60 s MCP request timeout.
pub const DEFAULT_SYNTHETIC_PROGRESS_INTERVAL_MS: u64 = 30_000;
/// Per-map cap for the `AuthorizationStore` LRUs. Equals [`core::constants::DEFAULT_LRU_SIZE`](crate::core::constants::DEFAULT_LRU_SIZE).
pub const AUTH_STORE_MAX_ENTRIES: usize = 5000;

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn methods_match_ts_sdk() {
assert_eq!(PAYMENT_REQUIRED_METHOD, "notifications/payment_required");
assert_eq!(PAYMENT_ACCEPTED_METHOD, "notifications/payment_accepted");
assert_eq!(PAYMENT_REJECTED_METHOD, "notifications/payment_rejected");
}

#[test]
fn error_codes_match_ts_sdk() {
assert_eq!(PAYMENT_REQUIRED_ERROR_CODE, -32042);
assert_eq!(PAYMENT_PENDING_ERROR_CODE, -32043);
assert_eq!(UNSUPPORTED_PAYMENT_INTERACTION_ERROR_CODE, -32602);
}

#[test]
fn pmi_and_ttls_match_ts_sdk() {
assert_eq!(PMI_BITCOIN_LIGHTNING_BOLT11, "bitcoin-lightning-bolt11");
assert_eq!(DEFAULT_PAYMENT_TTL_MS, 300_000);
assert_eq!(DEFAULT_SYNTHETIC_PROGRESS_INTERVAL_MS, 30_000);
assert_eq!(AUTH_STORE_MAX_ENTRIES, 5000);
// Mirrors the core LRU default; keep the two in lockstep.
assert_eq!(
AUTH_STORE_MAX_ENTRIES,
crate::core::constants::DEFAULT_LRU_SIZE
);
}
}
69 changes: 69 additions & 0 deletions src/payments/errors.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
//! Error taxonomy for CEP-8 payments.
//!
//! Mirrors the sibling per-module error enums
//! (`transport/open_stream/errors.rs`, `transport/oversized_transfer/errors.rs`)
//! so failure classification is consistent across the crate. Surfaced through
//! the crate-level [`crate::Error`] via a `#[from]` conversion on
//! `Error::Payment`.
//!
//! It is `#[non_exhaustive]` because it grows as later CEP-8 work adds variants
//! (canonicalization, payment selection and verification).
//! Unlike the all-`String` siblings it deliberately does not derive
//! `Clone`/`PartialEq`/`Eq`: it carries a [`serde_json::Error`], which is not `Clone`.

/// Errors raised while pricing, issuing, verifying, or paying a CEP-8 invocation.
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum PaymentError {
/// A server-side [`PaymentProcessor`](crate::payments::PaymentProcessor)
/// failed to create or verify a payment.
#[error("Payment processor error: {0}")]
Processor(String),

/// A client-side [`PaymentHandler`](crate::payments::PaymentHandler)
/// failed to execute a payment.
#[error("Payment handler error: {0}")]
Handler(String),

/// (De)serialization of a payment payload failed.
#[error("Payment serialization error: {0}")]
Serialization(#[from] serde_json::Error),
}

#[cfg(test)]
mod tests {
use super::*;
use crate::Error;

#[test]
fn folds_into_crate_error_payment_arm() {
let e: Error = PaymentError::Processor("x".to_string()).into();
assert!(matches!(e, Error::Payment(_)));
}

#[test]
fn display_nests_through_crate_error() {
let e: Error = PaymentError::Processor("x".to_string()).into();
assert_eq!(e.to_string(), "Payment error: Payment processor error: x");
}

#[test]
fn handler_variant_display() {
assert_eq!(
PaymentError::Handler("boom".to_string()).to_string(),
"Payment handler error: boom"
);
}

#[test]
fn serde_json_error_converts_via_question_mark() {
fn parse() -> Result<(), PaymentError> {
let _v: serde_json::Value = serde_json::from_str("{ not json")?;
Ok(())
}
assert!(matches!(
parse().unwrap_err(),
PaymentError::Serialization(_)
));
}
}
Loading
Loading