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
4 changes: 3 additions & 1 deletion crates/aleph-cli/src/commands/file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use crate::common::{resolve_account, resolve_address, submit_or_preview};
use aleph_sdk::client::{AlephClient, AlephStorageClient};
use aleph_sdk::messages::StoreBuilder;
use aleph_types::channel::Channel;
use aleph_types::message::execution::base::Payment;
use aleph_types::message::{FileRef, StorageEngine};
use url::Url;

Expand Down Expand Up @@ -57,7 +58,8 @@ async fn handle_file_upload(
}

// Build STORE message
let mut builder = StoreBuilder::new(&account, file_hash, storage_engine);
let mut builder =
StoreBuilder::new(&account, file_hash, storage_engine).payment(Payment::credits());
if let Some(owner) = args.on_behalf_of {
builder = builder.on_behalf_of(resolve_address(&owner)?);
}
Expand Down
10 changes: 9 additions & 1 deletion crates/aleph-sdk/src/messages.rs
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,7 @@ pub struct StoreBuilder<'a, A: Account> {
storage_engine: StorageEngine,
reference: Option<RawFileRef>,
metadata: Option<HashMap<String, serde_json::Value>>,
payment: Option<Payment>,
channel: Option<Channel>,
}

Expand All @@ -241,6 +242,7 @@ impl<'a, A: Account> StoreBuilder<'a, A> {
storage_engine,
reference: None,
metadata: None,
payment: None,
channel: None,
}
}
Expand Down Expand Up @@ -268,6 +270,12 @@ impl<'a, A: Account> StoreBuilder<'a, A> {
self
}

/// Set payment details for the STORE message.
pub fn payment(mut self, payment: Payment) -> Self {
self.payment = Some(payment);
self
}

/// Set the message channel.
pub fn channel(mut self, channel: Channel) -> Self {
self.channel = Some(channel);
Expand All @@ -289,7 +297,7 @@ impl<'a, A: Account> StoreBuilder<'a, A> {
}
};

let store_content = StoreContent::new(backend, self.reference, self.metadata);
let store_content = StoreContent::new(backend, self.reference, self.metadata, self.payment);
let value = serde_json::to_value(store_content)?;

let mut builder = MessageBuilder::new(self.account, MessageType::Store, value);
Expand Down
18 changes: 18 additions & 0 deletions crates/aleph-types/src/message/execution/base.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,24 @@ pub struct Payment {
pub payment_type: PaymentType,
}

impl Payment {
pub fn credits() -> Self {
Self {
chain: None,
receiver: None,
payment_type: PaymentType::Credit,
}
}

pub fn hold() -> Self {
Self {
chain: None,
receiver: None,
payment_type: PaymentType::Hold,
}
}
}

///Two types of program interfaces supported: plain binaries and ASGI apps.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
Expand Down
89 changes: 89 additions & 0 deletions crates/aleph-types/src/message/store.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::chain::Address;
use crate::cid::Cid;
use crate::item_hash::{AlephItemHash, ItemHash};
use crate::message::execution::base::{Payment, PaymentType};
use memsizes::Bytes;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
Expand Down Expand Up @@ -76,20 +77,34 @@ pub struct StoreContent {
/// Metadata of the VM.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub metadata: Option<HashMap<String, serde_json::Value>>,
/// Payment information for storage. Only `hold` and `credit` types are supported.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub payment: Option<Payment>,
}

impl StoreContent {
pub fn new(
file_hash: StorageBackend,
reference: Option<RawFileRef>,
metadata: Option<HashMap<String, serde_json::Value>>,
payment: Option<Payment>,
) -> Self {
Self {
file_hash,
size: None,
content_type: None,
reference,
metadata,
payment,
}
}

/// Returns `true` if the payment type is valid for a STORE message.
/// Only `hold` and `credit` are supported; `superfluid` is not allowed.
pub fn has_valid_payment(&self) -> bool {
match &self.payment {
None => true,
Some(p) => !matches!(p.payment_type, PaymentType::Superfluid),
}
}

Expand Down Expand Up @@ -205,4 +220,78 @@ mod tests {

assert_eq!(message, deserialized_message);
}

const TEST_HASH: &str = "d281eb8a69ba1f4dda2d71aaf3ded06caa92edd690ef3d0632f41aa91167762c";

#[test]
fn test_store_content_without_payment() {
let json = format!(r#"{{"item_type":"storage","item_hash":"{}"}}"#, TEST_HASH);
let content: StoreContent = serde_json::from_str(&json).unwrap();
assert!(content.payment.is_none());
assert!(content.has_valid_payment());
}

#[test]
fn test_store_content_with_credit_payment() {
let json = format!(
r#"{{"item_type":"storage","item_hash":"{}","payment":{{"type":"credit"}}}}"#,
TEST_HASH
);
let content: StoreContent = serde_json::from_str(&json).unwrap();
let payment = content.payment.as_ref().unwrap();
assert_eq!(payment.payment_type, PaymentType::Credit);
assert!(payment.chain.is_none());
assert!(payment.receiver.is_none());
assert!(content.has_valid_payment());
}

#[test]
fn test_store_content_with_hold_payment() {
let json = format!(
r#"{{"item_type":"storage","item_hash":"{}","payment":{{"type":"hold"}}}}"#,
TEST_HASH
);
let content: StoreContent = serde_json::from_str(&json).unwrap();
assert_eq!(
content.payment.as_ref().unwrap().payment_type,
PaymentType::Hold
);
assert!(content.has_valid_payment());
}

#[test]
fn test_store_content_superfluid_payment_invalid() {
let json = format!(
r#"{{"item_type":"storage","item_hash":"{}","payment":{{"type":"superfluid"}}}}"#,
TEST_HASH
);
let content: StoreContent = serde_json::from_str(&json).unwrap();
assert!(!content.has_valid_payment());
}

#[test]
fn test_store_content_credit_payment_round_trip() {
let json = format!(
r#"{{"item_type":"storage","item_hash":"{}","payment":{{"type":"credit"}}}}"#,
TEST_HASH
);
let content: StoreContent = serde_json::from_str(&json).unwrap();
let serialized = serde_json::to_string(&content).unwrap();
let deserialized: StoreContent = serde_json::from_str(&serialized).unwrap();
assert_eq!(content, deserialized);
}

#[test]
fn test_store_content_payment_not_serialized_when_none() {
let content = StoreContent::new(
StorageBackend::Storage {
item_hash: AlephItemHash::from_bytes(b"test"),
},
None,
None,
None,
);
let json = serde_json::to_string(&content).unwrap();
assert!(!json.contains("payment"));
}
}
8 changes: 8 additions & 0 deletions crates/heph/src/db/messages.rs
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,14 @@ impl DenormalizedFields {
if let Some(ref raw_ref) = store.reference {
fields.content_ref = Some(raw_ref.to_string());
}
// Extract payment type if present.
if let Some(ref payment) = store.payment
&& let Some(v) = serde_json::to_value(&payment.payment_type)
.ok()
.and_then(|v| v.as_str().map(|s| s.to_string()))
{
fields.payment_type = Some(v);
}
}
MessageContentEnum::Program(prog) => {
// Payment type is nested in on/payment objects — use a JSON value approach.
Expand Down
53 changes: 52 additions & 1 deletion crates/heph/src/handlers/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ pub fn process_store(
}
};

// Step 2 — validate payment type (only hold and credit allowed).
if !store_content.has_valid_payment() {
return Err(ProcessingError::InvalidPaymentMethod(
"only 'hold' and 'credit' payment types are supported for store messages".into(),
));
}

let file_hash = store_content.file_hash().to_string();
let owner = content.address.as_str().to_string();
let item_hash = msg.item_hash.to_string();
Expand Down Expand Up @@ -394,7 +401,51 @@ mod tests {
}

// -----------------------------------------------------------------------
// Test 7: FileStore write/read round-trip
// Test 7: STORE with credit payment succeeds
// -----------------------------------------------------------------------

#[test]
fn test_store_with_credit_payment() {
let key = [26u8; 32];
let addr = addr_for_key(&key);
let fh = fake_file_hash(12);
let ic = format!(
r#"{{"address":"{}","time":1000.0,"item_type":"storage","item_hash":"{}","payment":{{"type":"credit"}}}}"#,
addr, fh
);
let msg = sign_store_message(&key, 1_000.0, ic);
let db = Db::open_in_memory().unwrap();
process_message(&db, &msg).expect("store with credit payment should succeed");
}

// -----------------------------------------------------------------------
// Test 8: STORE with superfluid payment is rejected
// -----------------------------------------------------------------------

#[test]
fn test_store_with_superfluid_payment_rejected() {
let key = [27u8; 32];
let addr = addr_for_key(&key);
let fh = fake_file_hash(13);
let ic = format!(
r#"{{"address":"{}","time":1000.0,"item_type":"storage","item_hash":"{}","payment":{{"type":"superfluid"}}}}"#,
addr, fh
);
let msg = sign_store_message(&key, 1_000.0, ic);
let db = Db::open_in_memory().unwrap();
let result = process_message(&db, &msg);
assert!(result.is_err());
let err = result.unwrap_err();
assert_eq!(
err.error_code(),
202,
"expected InvalidPaymentMethod (202), got {:?}",
err
);
}

// -----------------------------------------------------------------------
// Test 9: FileStore write/read round-trip
// -----------------------------------------------------------------------

#[test]
Expand Down
Loading