Rust SDK Documentation
Complete reference for MPP-NEAR Rust SDK. Build payment-gated APIs with type-safe primitives.
Features
- ✅Stateless verification — HMAC-based challenge binding
- ✅Type-safe API — Builder patterns with compile-time checks
- ✅MPP-1.0 compliant — Spec-conformant implementation
- ✅Axum integration — Middleware & extractors included
- ✅Extensible — Custom payment methods via traits
Installation
# Cargo.toml
[dependencies]
mpp-near = { git = "https://github.com/kampouse/mpp-near", features = ["server"] }
tokio = { version = "1", features = ["full"] }
axum = "0.7"
serde = { version = "1", features = ["derive"] }
serde_json = "1"Core Types
Challenge
Payment requirements returned with 402
RequestData
Payment amount and details
Credential
Proof of payment from client
Receipt
Payment confirmation
Problem
RFC 9457 error responses
Verifier
Payment verification trait
Challenge Type
Fields
pub struct Challenge {
pub id: String, // Unique identifier (HMAC-derived)
pub realm: String, // Protection space (e.g., "api.example.com")
pub method: String, // Payment method (e.g., "near-intents")
pub intent: String, // Intent: "charge" | "session" | ...
pub request: String, // Base64url-encoded RequestData
pub expires: Option<String>, // RFC 3339 timestamp
pub digest: Option<String>, // RFC 9530 body digest
pub description: Option<String>, // Human-readable description
pub opaque: Option<String>, // Server correlation data
}ChallengeBuilder
use mpp_near::{Challenge, RequestData};
let challenge = Challenge::builder()
.realm("api.example.com")
.method("near-intents")
.intent("charge")
.request(RequestData::new("0.001", "wallet.near"))
.currency("USDC")
.expires(300) // TTL in seconds (default: 300)
.secret(b"your-hmac-secret")
.description("Image generation API")
.build()?;Builder Methods
| Method | Description |
|---|---|
realm() | Set protection space |
method() | Set payment method |
intent() | Set payment intent |
request() | Set RequestData |
expires() | Set TTL (seconds) |
digest() | Bind to request body |
description() | Human-readable description |
opaque() | Correlation data |
secret() | HMAC secret for signing |
RequestData Type
Fields
pub struct RequestData {
pub amount: String, // Decimal amount
pub currency: Option<String>, // Token symbol (e.g., "USDC")
pub token_id: Option<String>, // Contract address
pub recipient: String, // Recipient address
pub chain: Option<String>, // Blockchain/network
pub method_details: Option<Value>, // Method-specific data
pub extra: HashMap<String, Value>, // Additional fields
}Methods
use mpp_near::RequestData;
// Create with required fields
let request = RequestData::new("0.001", "wallet.near");
// Builder pattern for optional fields
let request = RequestData::new("0.001", "wallet.near")
.currency("USDC")
.token_id("usdc.contract.near")
.chain("near")
.method_details(json!({"swap": "auto"}))
.extra("max_slippage", json!("0.5"))
.encode()?; // Encode to base64urlCredential Type
Fields
pub struct Credential {
pub challenge: ChallengeEcho, // Challenge reference
pub proof: PaymentProof, // Method-specific proof
pub expires: String, // RFC 3339 timestamp
pub signature: String, // Client signature
}Creating Credentials (Client-side)
use mpp_near::Credential;
let credential = Credential::builder()
.challenge(&challenge)
.proof("intent_hash_from_payment")
.sign(client_private_key)?
.build()?;Verifying Credentials (Server-side)
use mpp_near::{Credential, VerificationResult};
// From Authorization header
let credential = Credential::from_authorization(auth_header)?;
// Verify challenge binding
match credential.verify(&challenge) {
VerificationResult::Valid => {
// Payment verified successfully
},
VerificationResult::Invalid(reason) => {
// Payment verification failed
return Err(reason);
},
}Receipt Type
Issue receipts to confirm successful payments. Return via Payment-Receipt header.
use mpp_near::Receipt;
let receipt = Receipt::builder()
.challenge_id(&challenge.id)
.payer(Some("user.near"))
.amount("0.001")
.currency("USDC")
.intent("charge")
.build()?;
// Convert to header value
let header_value = receipt.to_header();
response.headers.insert("Payment-Receipt", header_value);Receipt Fields
| Field | Type | Description |
|---|---|---|
challenge_id | String | Reference to challenge |
payer | Option<String> | Payer address |
amount | String | Amount paid |
currency | String | Token symbol |
intent | String | Payment intent |
timestamp | String | ISO 8601 timestamp |
PaymentProof Type
Payment proof contains method-specific payment data from the credential.
pub struct PaymentProof {
pub proof: String, // Transaction hash
pub account: Option<String>, // Payer account
pub signature: Option<String>, // Transaction signature
pub public_key: Option<String>, // Payer public key
pub extra: HashMap<String, Value>, // Additional data
}Creating PaymentProof
use mpp_near::PaymentProof;
// Simple proof
let proof = PaymentProof::new("intent_hash_here");
// From credential payload (JSON)
let proof = PaymentProof::from_payload(&credential_json)?;Body Digest (RFC 9530)
Bind challenges to request bodies to prevent tampering. Clients cannot modify the body after receiving a challenge.
pub struct BodyDigest {
pub algorithm: DigestAlgorithm, // Sha256 or Sha512
pub hash: String, // Base64-encoded hash
}
pub enum DigestAlgorithm {
Sha256, // SHA-256
Sha512, // SHA-512
}Creating Body Digests
use mpp_near::BodyDigest;
// From request body
let body = b"{\"prompt\": \"generate image\"}";
let digest = BodyDigest::sha256(body);
// Format: "sha-256=:base64hash:"
let digest_header = digest.to_header();
// Add to challenge
let challenge = Challenge::builder()
.request(RequestData::new("0.001", "wallet.near"))
.body_digest(digest)
.build()?;Supported Algorithms
| Algorithm | Use Case |
|---|---|
sha-256 | Default, fast |
sha-512 | Higher security |
HTTP Headers
Response Headers (402)
| Header | Description |
|---|---|
WWW-Authenticate | Challenge (base64) |
Payment-Required | Always "402" |
Cache-Control | "no-store" (prevent caching) |
Request Headers (Payment)
| Header | Description |
|---|---|
Authorization | Credential (base64) |
Content-Digest | Body digest (if binding) |
Response Headers (200)
| Header | Description |
|---|---|
Payment-Receipt | Receipt (base64) |
Verification
Verifier Trait
Implement custom verification logic via the Verifier trait:
use mpp_near::Verifier;
pub trait Verifier {
fn verify(&self, credential: &Credential)
-> VerificationResult;
}
// Implementation provided for:
// - Challenge binding verification
// - Signature validation
// - Expiry checkingVerificationResult
pub enum VerificationResult {
Valid, // Payment verified
Invalid(String), // Verification failed with reason
Expired, // Challenge/expired
}Custom Payment Methods
Implement custom payment methods via the Method trait. This allows integration with any payment network.
Method Trait
use mpp_near::Method;
use async_trait::async_trait;
#[async_trait]
pub trait Method: Send + Sync {
/// Method identifier (e.g., "near-intents", "stripe")
fn id(&self) -> &str;
/// Build a challenge for this method
fn build_challenge(
&self,
request: &PaymentRequest,
secret: &[u8]
) -> Result<Challenge>;
/// Verify a payment proof
async fn verify(
&self,
request: &PaymentRequest,
proof: &PaymentProof,
) -> Result<bool>;
/// Extract request from challenge
fn extract_request(
&self,
challenge: &Challenge
) -> Result<PaymentRequest>;
/// Extract proof from credential
fn extract_proof(
&self,
credential: &Credential
) -> Result<PaymentProof>;
/// Verify full credential
async fn verify_credential(
&self,
challenge: &Challenge,
credential: &Credential,
) -> Result<bool>;
}PaymentRequest
pub struct PaymentRequest {
pub amount: String, // Amount to pay
pub currency: Option<String>, // Currency/token
pub token_id: Option<String>, // Token contract
pub recipient: String, // Recipient address
pub chain: Option<String>, // Blockchain/network
pub challenge_id: String, // Challenge reference
pub realm: String, // Protection space
pub method: String, // Payment method
pub intent: String, // Payment intent
}PaymentProof
pub struct PaymentProof {
pub proof: String, // Transaction hash
pub account: Option<String>, // Payer account
pub signature: Option<String>, // Transaction signature
pub public_key: Option<String>, // Payer public key
pub extra: HashMap<String, Value>, // Additional fields
}MethodRegistry
Register and manage multiple payment methods:
use mpp_near::MethodRegistry;
let mut registry = MethodRegistry::new();
registry.register(MyCustomMethod);
registry.register(AnotherMethod);
// Check if method exists
if registry.contains("my-method") {
let method = registry.get("my-method").unwrap();
// Use method to verify payment
}
// List all methods
let methods = registry.list();Registry Methods
| Method | Description |
|---|---|
new() | Create empty registry |
register() | Register a method |
get() | Get method by ID |
contains() | Check if method exists |
list() | List all method IDs |
Error Handling
Problem Type (RFC 9457)
Return structured error responses with Problem:
use mpp_near::Problem;
// Payment insufficient
let problem = Problem::payment_insufficient(
"0.01", // required
"0.001", // actual
);
// Verification failed
let problem = Problem::verification_failed(
"Invalid signature"
);
// Invalid challenge
let problem = Problem::invalid_challenge(
"Challenge expired"
);
// Returns JSON:
// {
// "type": "https://mpp.dev/problems/payment-insufficient",
// "title": "Payment Insufficient",
// "detail": "Required: 0.01, Actual: 0.001",
// "status": 402
// }
let json = serde_json::to_string(&problem)?;Error Types (Problem)
| Type | Description |
|---|---|
payment_insufficient | Amount too low |
verification_failed | Invalid proof/signature |
invalid_challenge | Challenge malformed |
challenge_expired | Challenge too old |
unsupported_method | Unknown payment method |
Error Enum
Full error enum with all variants:
pub enum Error {
InvalidChallenge(String), // Invalid challenge format
InvalidCredential(String), // Invalid credential format
ChallengeExpired, // Challenge expired
ChallengeNotFound, // Challenge not found
VerificationFailed(String), // Payment verification failed
UnsupportedMethod(String), // Unsupported payment method
InvalidAmount(String), // Invalid amount
Http(http::Error), // HTTP error
Json(serde_json::Error), // JSON error
Base64(base64::DecodeError), // Base64 decode error
Other(String), // Other error
}Result Type
Type alias for Result with Error:
use mpp_near::Result;
// Result type alias
pub type Result<T> = std::result::Result<T, Error>;
// Usage
fn create_challenge() -> Result<Challenge> {
// Returns Result<Challenge, Error>
Ok(challenge)
}Constants
MPP-NEAR provides protocol-level constants for versioning and defaults:
/// MPP protocol version
pub const VERSION: &str = "MPP/1.0";
/// Default challenge TTL in seconds
pub const DEFAULT_CHALLENGE_TTL: i64 = 300; // 5 minutesUsage
use mpp_near::{VERSION, DEFAULT_CHALLENGE_TTL};
// Use in responses
response.headers.insert("MPP-Version", VERSION.parse()?);
// Use for default TTL
let ttl = DEFAULT_CHALLENGE_TTL; // 300 seconds
// Override as needed
let challenge = Challenge::builder()
.expires(DEFAULT_CHALLENGE_TTL)
.build()?;Note
VERSION is included in all challenges and credentials for protocol negotiation. DEFAULT_CHALLENGE_TTL is used when no explicit TTL is set via .expires().
Complete Example
Payment-Gated API Endpoint
use axum::{Json, http::StatusCode, extract::State};
use mpp_near::{Challenge, Credential, RequestData, Receipt};
use std::sync::Arc;
async fn protected_image_generation(
State(secret): State<Arc<Vec<u8>>>,
) -> Result<Json<&'static str>, StatusCode> {
// Create payment challenge
let challenge = Challenge::builder()
.realm("api.example.com")
.method("near-intents")
.intent("charge")
.request(
RequestData::new("0.001", "wallet.near")
.currency("USDC")
)
.description("AI image generation")
.secret(&secret)
.build()?;
// Return 402 with challenge
Err(StatusCode::PAYMENT_REQUIRED)
// Include WWW-Authenticate header with challenge
}Axum Middleware
Pre-built Axum middleware and extractors are available:
// Middleware features:
- Payment verification middleware
- Credential extractor
- Challenge builder helper
- Problem response helper
// Available with:
mpp-near = { git = "https://github.com/kampouse/mpp-near",
features = ["server"] }