From 5ebf3a41ac85390bcb4f9e362babe29d1dca0e69 Mon Sep 17 00:00:00 2001
From: Nic-dorman
Date: Fri, 17 Apr 2026 17:59:02 +0100
Subject: [PATCH] feat: wire public-visibility uploads through external-signer
flow
Threads a visibility flag through start_upload -> file_prepare_upload_with_visibility
(ant-core PR #39), carries data_map_address out of finalize_upload and
finalize_upload_merkle, persists it in upload_history.json, and re-enables the
previously disabled "Public" button in the upload dialog.
Public uploads now yield a single shareable on-network chunk address that
appears in the uploads table with a "Public" badge; clicking copies it. The
dialog re-quotes when the user toggles visibility because the payment batch
differs (public adds one chunk for the DataMap itself).
Requires bumping the ant-core git dep to a commit containing WithAutonomi/ant-client#39.
Co-Authored-By: Claude Opus 4.7 (1M context)
---
components/files/UploadConfirmDialog.vue | 31 ++++++++++----
pages/files.vue | 54 ++++++++++++++++++++++--
src-tauri/src/autonomi_ops.rs | 31 +++++++++++++-
src-tauri/src/config.rs | 4 ++
stores/files.ts | 38 ++++++++++++++---
5 files changed, 137 insertions(+), 21 deletions(-)
diff --git a/components/files/UploadConfirmDialog.vue b/components/files/UploadConfirmDialog.vue
index 3511c45..4a87ad7 100644
--- a/components/files/UploadConfirmDialog.vue
+++ b/components/files/UploadConfirmDialog.vue
@@ -113,21 +113,26 @@
-
+
- Not yet available. All uploads are currently private.
+ Data map is published to the network. Share a single address so anyone can retrieve the file.
@@ -185,12 +190,22 @@ const props = defineProps<{
const emit = defineEmits<{
confirm: [options: { visibility: 'private' | 'public'; paymentMode: 'regular' | 'merkle' }]
cancel: []
+ /**
+ * Fired whenever the user flips the Private/Public selector. The parent
+ * re-quotes against the network since the prepared payment batch differs
+ * — public uploads pay for one extra chunk (the data map itself).
+ */
+ 'visibility-change': [visibility: 'private' | 'public']
}>()
const connectionStore = useConnectionStore()
const visibility = ref<'private' | 'public'>('private')
+watch(visibility, (val) => {
+ emit('visibility-change', val)
+})
+
const totalSize = computed(() => props.files.reduce((sum, f) => sum + f.size, 0))
const estimatedChunks = computed(() => Math.max(1, Math.ceil(totalSize.value / AVG_CHUNK_SIZE)))
diff --git a/pages/files.vue b/pages/files.vue
index a0eeee6..d1afe96 100644
--- a/pages/files.vue
+++ b/pages/files.vue
@@ -121,7 +121,16 @@
+ Public
+ {{ truncateAddress(file.public_address, 8, 6) }}
+
+
(null)
const quotedGasEstimate = ref(null)
const quotedPaymentMode = ref<'wave-batch' | 'merkle' | null>(null)
const pendingQuotes = ref>(new Map())
+/** Visibility the currently-pinned quotes were collected under. Re-quoting
+ * is required when the user flips Private/Public because ant-core bundles
+ * the data map chunk into the payment batch for public uploads, so the
+ * prepared chunks and the total cost both change. */
+const quotedVisibility = ref<'private' | 'public'>('private')
async function getFileMetas(paths: string[]): Promise {
try {
@@ -468,6 +483,7 @@ async function showUploadConfirmForPaths(paths: string[]) {
quotedGasEstimate.value = null
quotedPaymentMode.value = null
pendingQuotes.value = new Map()
+ quotedVisibility.value = 'private'
showUploadConfirm.value = true
const metas = await getFileMetas(paths)
@@ -505,13 +521,14 @@ watch(
},
)
-async function startQuoting(metas: FileMeta[]) {
+async function startQuoting(metas: FileMeta[], visibility: 'private' | 'public' = quotedVisibility.value) {
isQuoting.value = true
+ quotedVisibility.value = visibility
try {
// Quote each file (sequentially to avoid overwhelming the network)
const quotes = new Map()
for (const meta of metas) {
- const quote = await filesStore.getUploadQuote(meta.path)
+ const quote = await filesStore.getUploadQuote(meta.path, visibility)
if (quote) {
quotes.set(meta.path, quote)
}
@@ -558,9 +575,15 @@ function confirmUpload(options: { visibility: 'private' | 'public'; paymentMode:
showUploadConfirm.value = false
const wagmiConfig = getWagmiConfig()
+ // The pre-quote's prepared batch is tied to its visibility (public bundles
+ // the data map chunk). If the user toggled visibility after the quote came
+ // back and we haven't finished re-quoting, drop the pre-quote so the store
+ // re-quotes with the matching visibility instead of reusing a stale one.
+ const preQuoteUsable = quotedVisibility.value === options.visibility
+
for (const file of pendingUploadFiles.value) {
const id = filesStore.addUpload(file.name, file.path, file.size)
- const preQuote = pendingQuotes.value.get(file.path)
+ const preQuote = preQuoteUsable ? pendingQuotes.value.get(file.path) : undefined
if (settingsStore.indelibleConnected && !settingsStore.devnetActive) {
filesStore.startIndelibleUpload(id)
@@ -584,6 +607,24 @@ function cancelUpload() {
isQuoting.value = false
}
+/**
+ * Re-quote when the user flips Private/Public in the dialog. Public uploads
+ * pay for one extra chunk (the published data map), so both the total cost
+ * and the underlying prepared batch differ from the initial private quote.
+ * The confirm handler also refuses to reuse a quote whose visibility no
+ * longer matches the user's choice.
+ */
+function onVisibilityChange(next: 'private' | 'public') {
+ if (next === quotedVisibility.value) return
+ quotedVisibility.value = next
+ if (pendingMetas.value.length === 0) return
+ if (!autonomiConnected.value && !settingsStore.devnetActive) return
+ if (settingsStore.indelibleConnected) return
+ quotedCostDisplay.value = null
+ quotedGasEstimate.value = null
+ startQuoting(pendingMetas.value, next)
+}
+
// ── Download flow ──
const showDownloadDialog = ref(false)
@@ -810,6 +851,11 @@ function copyAddress(addr: string) {
toastStore.add('Address copied to clipboard', 'info')
}
+function copyPublicAddress(addr: string) {
+ navigator.clipboard.writeText(addr)
+ toastStore.add('Public address copied — share to let others download this file', 'info')
+}
+
function datamapBasename(path: string): string {
return path.split(/[\\/]/).pop() ?? path
}
diff --git a/src-tauri/src/autonomi_ops.rs b/src-tauri/src/autonomi_ops.rs
index 4db772c..0bfbd6a 100644
--- a/src-tauri/src/autonomi_ops.rs
+++ b/src-tauri/src/autonomi_ops.rs
@@ -1,5 +1,6 @@
use ant_core::data::{
Client, ClientConfig, CustomNetwork, DataMap, EvmNetwork, ExternalPaymentInfo, PreparedUpload,
+ Visibility,
};
use evmlib::common::{QuoteHash, TxHash};
use serde::{Deserialize, Serialize};
@@ -161,12 +162,21 @@ pub struct UploadResult {
/// This is the user-visible handle for private uploads — without it, the
/// data is unreachable after the app restarts.
pub data_map_file: String,
+ /// On-network chunk address of the published `DataMap`, set only for
+ /// public uploads. A shareable 32-byte hex string anyone can pass to
+ /// `download_public` to retrieve the file without a local datamap.
+ pub public_address: Option,
}
#[derive(Deserialize)]
pub struct StartUploadRequest {
pub files: Vec,
pub upload_id: String,
+ /// Upload visibility — "private" (default) keeps the data map local,
+ /// "public" bundles the serialized data map into the payment batch so
+ /// a single on-network chunk address can be shared for retrieval.
+ #[serde(default)]
+ pub visibility: Option,
}
// ── Tauri commands ──
@@ -385,9 +395,18 @@ pub async fn start_upload(
}
let path = canonical;
- // Phase 1: Encrypt file and prepare chunks (gets quotes from network)
+ let visibility = match request.visibility.as_deref() {
+ Some("public") => Visibility::Public,
+ _ => Visibility::Private,
+ };
+
+ // Phase 1: Encrypt file and prepare chunks (gets quotes from network).
+ // For a public upload, ant-core bundles the serialized DataMap into the
+ // payment batch as one extra chunk so it's paid for and stored alongside
+ // the data chunks. The resulting chunk address is surfaced via
+ // `FileUploadResult.data_map_address` after finalize.
let prepared = client
- .file_prepare_upload(&path)
+ .file_prepare_upload_with_visibility(&path, visibility)
.await
.map_err(|e| format!("Failed to prepare upload: {e}"))?;
@@ -540,6 +559,9 @@ pub async fn confirm_upload(
let data_map_file = crate::config::write_datamap_for(&file_name, &data_map_json)?
.to_string_lossy()
.into_owned();
+ let public_address = result
+ .data_map_address
+ .map(|addr| format!("0x{}", hex::encode(addr)));
app.emit(
"upload-progress",
@@ -557,6 +579,7 @@ pub async fn confirm_upload(
address,
chunks_stored: result.chunks_stored,
data_map_file,
+ public_address,
})
}
@@ -601,6 +624,9 @@ pub async fn confirm_upload_merkle(
let data_map_file = crate::config::write_datamap_for(&file_name, &data_map_json)?
.to_string_lossy()
.into_owned();
+ let public_address = result
+ .data_map_address
+ .map(|addr| format!("0x{}", hex::encode(addr)));
app.emit(
"upload-progress",
@@ -618,6 +644,7 @@ pub async fn confirm_upload_merkle(
address,
chunks_stored: result.chunks_stored,
data_map_file,
+ public_address,
})
}
diff --git a/src-tauri/src/config.rs b/src-tauri/src/config.rs
index d31ac2f..1034b73 100644
--- a/src-tauri/src/config.rs
+++ b/src-tauri/src/config.rs
@@ -50,6 +50,10 @@ pub struct UploadHistoryEntry {
/// persistence existed.
#[serde(default)]
pub data_map_file: Option,
+ /// On-network chunk address of the published `DataMap` for public
+ /// uploads. `None` for private uploads and for legacy entries.
+ #[serde(default)]
+ pub public_address: Option,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
diff --git a/stores/files.ts b/stores/files.ts
index a3bf197..fb221f9 100644
--- a/stores/files.ts
+++ b/stores/files.ts
@@ -50,6 +50,10 @@ export interface FileEntry {
size_bytes: number
/** Network address (hex) — for display / sharing */
address?: string
+ /** On-network chunk address of the published DataMap, set for public
+ * uploads. This is the single shareable handle — anyone with it can
+ * fetch the DataMap via `download_public` and decrypt the file. */
+ public_address?: string
/** Serialized DataMap JSON — needed for download from network */
data_map_json?: string
/** Absolute path to the persisted DataMap file on disk. Set for private
@@ -89,6 +93,9 @@ export interface UploadHistoryEntry {
/** Absolute path to the persisted DataMap file; `null`/absent for legacy
* entries written before datamap persistence was added. */
data_map_file?: string | null
+ /** On-network chunk address of the published DataMap for public uploads.
+ * `null`/absent for private uploads and for legacy entries. */
+ public_address?: string | null
}
export const useFilesStore = defineStore('files', {
@@ -143,6 +150,7 @@ export const useFilesStore = defineStore('files', {
name: e.name,
size_bytes: e.size_bytes,
address: e.address,
+ public_address: e.public_address ?? undefined,
cost: e.cost ?? undefined,
data_map_file: e.data_map_file ?? undefined,
status: 'complete',
@@ -168,6 +176,7 @@ export const useFilesStore = defineStore('files', {
cost: f.cost ?? null,
uploaded_at: f.date,
data_map_file: f.data_map_file ?? null,
+ public_address: f.public_address ?? null,
}))
try {
@@ -228,8 +237,18 @@ export const useFilesStore = defineStore('files', {
return id
},
- /** Get a real network quote for a file. Used by the upload dialog to show real costs. */
- async getUploadQuote(path: string): Promise {
+ /**
+ * Get a real network quote for a file. Used by the upload dialog to show
+ * real costs.
+ *
+ * `visibility` controls whether ant-core bundles the serialized DataMap
+ * into the payment batch. Public quotes cost slightly more than private
+ * ones because the DataMap is billed as one extra chunk. A quote obtained
+ * with one visibility must be finalized with the same visibility — the
+ * prepared chunks on the backend differ — so callers re-quote if the
+ * user changes their selection.
+ */
+ async getUploadQuote(path: string, visibility: 'private' | 'public' = 'private'): Promise {
const uploadId = `quote-${Date.now()}-${Math.random().toString(36).slice(2)}`
// Deferred so the listener can resolve independently of where the
@@ -259,7 +278,7 @@ export const useFilesStore = defineStore('files', {
})
await invoke('start_upload', {
- request: { files: [path], upload_id: uploadId },
+ request: { files: [path], upload_id: uploadId, visibility },
})
const quote = await quotePromise
@@ -296,7 +315,10 @@ export const useFilesStore = defineStore('files', {
let quote: UploadQuote
if (preQuote) {
- // Use pre-obtained quote (from dialog phase)
+ // Use pre-obtained quote (from dialog phase). The caller is
+ // responsible for ensuring its visibility matches `options.visibility`
+ // — `pages/files.vue` drops the pre-quote when the user changes
+ // visibility so we always re-quote with the matching batch.
uploadId = preQuote.upload_id
quote = preQuote
this.updateEntry(id, { cost: preQuote.total_cost_display })
@@ -305,7 +327,7 @@ export const useFilesStore = defineStore('files', {
// there is a single implementation of the listen+invoke dance.
if (!entry.path) throw new Error('Upload entry has no file path')
this.updateEntry(id, { status: 'quoting' })
- const fresh = await this.getUploadQuote(entry.path)
+ const fresh = await this.getUploadQuote(entry.path, options.visibility)
if (!fresh) throw new Error('Failed to get quote from network')
uploadId = fresh.upload_id
quote = fresh
@@ -339,7 +361,7 @@ export const useFilesStore = defineStore('files', {
// can legitimately take many minutes for larger files. The CLI
// (which works) also has no timeout here. Backend errors still
// surface through invoke's rejection.
- const result = await invoke<{ upload_id: string; data_map_json: string; address: string; chunks_stored: number; data_map_file: string }>('confirm_upload_merkle', {
+ const result = await invoke<{ upload_id: string; data_map_json: string; address: string; chunks_stored: number; data_map_file: string; public_address: string | null }>('confirm_upload_merkle', {
uploadId,
winnerPoolHash: payResult.winnerPoolHash,
})
@@ -351,6 +373,7 @@ export const useFilesStore = defineStore('files', {
status: 'complete',
progress: 100,
address: result.address,
+ public_address: result.public_address ?? undefined,
data_map_json: result.data_map_json,
data_map_file: result.data_map_file,
duration,
@@ -383,7 +406,7 @@ export const useFilesStore = defineStore('files', {
this.updateEntry(id, { status: 'uploading', progress: 0 })
// No frontend timeout — see confirm_upload_merkle above for rationale.
- const result = await invoke<{ upload_id: string; data_map_json: string; address: string; chunks_stored: number; data_map_file: string }>('confirm_upload', {
+ const result = await invoke<{ upload_id: string; data_map_json: string; address: string; chunks_stored: number; data_map_file: string; public_address: string | null }>('confirm_upload', {
uploadId,
txHashes,
})
@@ -395,6 +418,7 @@ export const useFilesStore = defineStore('files', {
status: 'complete',
progress: 100,
address: result.address,
+ public_address: result.public_address ?? undefined,
data_map_json: result.data_map_json,
data_map_file: result.data_map_file,
duration,