From 155c4842c653daf10079ca1153a5fa196b765f20 Mon Sep 17 00:00:00 2001 From: Nic-dorman Date: Mon, 30 Mar 2026 11:45:27 +0100 Subject: [PATCH] feat(external-signer): add data_prepare_upload for in-memory data MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit file_prepare_upload exists for file-based external signer flow but there was no equivalent for raw byte data (data_upload). This meant external signers could only upload files, not in-memory content. Add data_prepare_upload(content: Bytes) -> PreparedUpload that: 1. Encrypts content via self-encryption 2. Collects quotes for each chunk via prepare_chunk_payment() 3. Returns PreparedUpload with PaymentIntent for external signing The caller then signs externally and calls finalize_upload() with tx hashes — same phase 2 as file uploads. Also adds compile-time Send assertion for the new method. Co-Authored-By: Claude Opus 4.6 (1M context) --- ant-core/src/data/client/data.rs | 58 ++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/ant-core/src/data/client/data.rs b/ant-core/src/data/client/data.rs index d27b148..f9a3cae 100644 --- a/ant-core/src/data/client/data.rs +++ b/ant-core/src/data/client/data.rs @@ -6,6 +6,8 @@ //! For file-based streaming uploads that avoid loading the entire //! file into memory, see the `file` module. +use crate::data::client::batch::PaymentIntent; +use crate::data::client::file::PreparedUpload; use crate::data::client::merkle::PaymentMode; use crate::data::client::Client; use crate::data::error::{Error, Result}; @@ -147,6 +149,56 @@ impl Client { } } + /// Phase 1 of external-signer data upload: encrypt and collect quotes. + /// + /// Encrypts in-memory data via self-encryption, then collects storage + /// quotes for each chunk without making any on-chain payment. Returns + /// a [`PreparedUpload`] containing the data map and a [`PaymentIntent`] + /// with the payment details for external signing. + /// + /// After the caller signs and submits the payment transaction, call + /// [`Client::finalize_upload`] with the tx hashes to complete storage. + /// + /// # Errors + /// + /// Returns an error if encryption fails or quote collection fails. + pub async fn data_prepare_upload(&self, content: Bytes) -> Result { + let content_len = content.len(); + debug!("Preparing data upload for external signing ({content_len} bytes)"); + + let (data_map, encrypted_chunks) = encrypt(content) + .map_err(|e| Error::Encryption(format!("Failed to encrypt data: {e}")))?; + + let chunk_count = encrypted_chunks.len(); + info!("Data encrypted into {chunk_count} chunks"); + + let chunk_contents: Vec = encrypted_chunks + .into_iter() + .map(|chunk| chunk.content) + .collect(); + + let mut prepared_chunks = Vec::with_capacity(chunk_contents.len()); + for content in chunk_contents { + if let Some(prepared) = self.prepare_chunk_payment(content).await? { + prepared_chunks.push(prepared); + } + } + + let payment_intent = PaymentIntent::from_prepared_chunks(&prepared_chunks); + + info!( + "Data prepared for external signing: {} chunks, total {} atto ({content_len} bytes)", + prepared_chunks.len(), + payment_intent.total_amount, + ); + + Ok(PreparedUpload { + data_map, + prepared_chunks, + payment_intent, + }) + } + /// Store a `DataMap` on the network as a public chunk. /// /// The serialized `DataMap` is stored as a regular content-addressed chunk. @@ -270,4 +322,10 @@ mod send_assertions { let fut = client.data_upload_with_mode(Bytes::new(), PaymentMode::Auto); _assert_send(&fut); } + + #[allow(dead_code, unreachable_code, clippy::diverging_sub_expression)] + async fn _data_prepare_upload_is_send(client: &Client) { + let fut = client.data_prepare_upload(Bytes::new()); + _assert_send(&fut); + } }