Skip to content
Draft
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
31 changes: 23 additions & 8 deletions components/files/UploadConfirmDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -113,21 +113,26 @@
</p>
</button>

<!-- Public (disabled — not yet implemented in backend) -->
<!-- Public -->
<button
type="button"
disabled
aria-disabled="true"
title="Public uploads are not yet available"
class="flex-1 cursor-not-allowed rounded-lg border border-autonomi-border p-3 text-left opacity-50"
class="flex-1 rounded-lg border p-3 text-left transition-all"
:class="visibility === 'public'
? 'border-autonomi-blue bg-autonomi-blue/10'
: 'border-autonomi-border hover:border-autonomi-blue/30'"
@click="visibility = 'public'"
>
<div class="flex items-center gap-2">
<div class="flex h-4 w-4 items-center justify-center rounded-full border-2 border-autonomi-muted" />
<div
class="flex h-4 w-4 items-center justify-center rounded-full border-2"
:class="visibility === 'public' ? 'border-autonomi-blue' : 'border-autonomi-muted'"
>
<div v-if="visibility === 'public'" class="h-2 w-2 rounded-full bg-autonomi-blue" />
</div>
<span class="text-sm font-medium">Public</span>
<span class="ml-auto rounded bg-autonomi-border/60 px-1.5 py-0.5 text-[10px] uppercase tracking-wider text-autonomi-muted">Coming soon</span>
</div>
<p class="mt-1.5 pl-6 text-xs text-autonomi-muted">
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.
</p>
</button>
</div>
Expand Down Expand Up @@ -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)))

Expand Down
54 changes: 50 additions & 4 deletions pages/files.vue
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,16 @@
</td>
<td class="px-4 py-2.5">
<span
v-if="file.data_map_file"
v-if="file.public_address"
class="inline-flex cursor-pointer items-center gap-1.5 font-mono text-xs text-autonomi-blue hover:text-autonomi-blue/80"
title="Public upload — click to copy the shareable network address"
@click.stop="copyPublicAddress(file.public_address)"
>
<span class="rounded bg-autonomi-blue/15 px-1 py-px text-[9px] font-sans uppercase tracking-wider">Public</span>
{{ truncateAddress(file.public_address, 8, 6) }}
</span>
<span
v-else-if="file.data_map_file"
class="cursor-pointer font-mono text-xs text-autonomi-muted hover:text-autonomi-blue"
:title="`Reveal ${datamapBasename(file.data_map_file)} in its folder`"
@click.stop="openFolder(file.data_map_file)"
Expand Down Expand Up @@ -239,6 +248,7 @@
:network-connected="autonomiConnected"
@confirm="confirmUpload"
@cancel="cancelUpload"
@visibility-change="onVisibilityChange"
/>

<FilesCostEstimateDialog
Expand Down Expand Up @@ -432,6 +442,11 @@ const quotedCostDisplay = ref<string | null>(null)
const quotedGasEstimate = ref<string | null>(null)
const quotedPaymentMode = ref<'wave-batch' | 'merkle' | null>(null)
const pendingQuotes = ref<Map<string, UploadQuote>>(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<FileMeta[]> {
try {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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<string, UploadQuote>()
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)
}
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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
}
Expand Down
31 changes: 29 additions & 2 deletions src-tauri/src/autonomi_ops.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down Expand Up @@ -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<String>,
}

#[derive(Deserialize)]
pub struct StartUploadRequest {
pub files: Vec<String>,
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<String>,
}

// ── Tauri commands ──
Expand Down Expand Up @@ -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}"))?;

Expand Down Expand Up @@ -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",
Expand All @@ -557,6 +579,7 @@ pub async fn confirm_upload(
address,
chunks_stored: result.chunks_stored,
data_map_file,
public_address,
})
}

Expand Down Expand Up @@ -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",
Expand All @@ -618,6 +644,7 @@ pub async fn confirm_upload_merkle(
address,
chunks_stored: result.chunks_stored,
data_map_file,
public_address,
})
}

Expand Down
4 changes: 4 additions & 0 deletions src-tauri/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ pub struct UploadHistoryEntry {
/// persistence existed.
#[serde(default)]
pub data_map_file: Option<String>,
/// 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<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
Expand Down
38 changes: 31 additions & 7 deletions stores/files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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', {
Expand Down Expand Up @@ -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',
Expand All @@ -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 {
Expand Down Expand Up @@ -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<UploadQuote | null> {
/**
* 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<UploadQuote | null> {
const uploadId = `quote-${Date.now()}-${Math.random().toString(36).slice(2)}`

// Deferred so the listener can resolve independently of where the
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 })
Expand All @@ -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
Expand Down Expand Up @@ -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,
})
Expand All @@ -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,
Expand Down Expand Up @@ -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,
})
Expand All @@ -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,
Expand Down
Loading