Skip to content
Closed
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
25 changes: 21 additions & 4 deletions src/cascade/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,14 +229,31 @@ export class SNApiClient {
* ```
*/
async getTaskStatus(taskId: string): Promise<TaskStatus> {
// Prefer the versioned path; fall back to legacy/non-versioned path on 404
// `/status` is SSE on sn-api-server and can block when polled via fetch text.
// Use `/history` for polling and return the latest status entry.
try {
return await this.http.get(`/api/v1/actions/cascade/tasks/${taskId}/status`);
const history = await this.http.get<Array<Record<string, unknown>>>(`/api/v1/actions/cascade/tasks/${taskId}/history`);
if (Array.isArray(history) && history.length > 0) {
return history[history.length - 1] as unknown as TaskStatus;
}
} catch (err) {
if (err instanceof HttpError && err.statusCode === 404) {
if (!(err instanceof HttpError && err.statusCode === 404)) {
throw err;
}
const legacyHistory = await this.http.get<Array<Record<string, unknown>>>(`/api/actions/cascade/tasks/${taskId}/history`);
if (Array.isArray(legacyHistory) && legacyHistory.length > 0) {
return legacyHistory[legacyHistory.length - 1] as unknown as TaskStatus;
}
}

// Fallback to explicit status endpoint for deployments where it is plain JSON.
try {
return await this.http.get(`/api/v1/actions/cascade/tasks/${taskId}/status`);
} catch (statusErr) {
if (statusErr instanceof HttpError && statusErr.statusCode === 404) {
return this.http.get(`/api/actions/cascade/tasks/${taskId}/status`);
}
throw err;
throw statusErr;
}
}

Expand Down
53 changes: 45 additions & 8 deletions src/cascade/uploader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ import { toBase64, toCanonicalJsonBytes } from '../internal/encoding';
import { createSingleBlockLayout, generateIds, buildIndexFile } from '../wasm/lep1';
import type { UniversalSigner, ArbitrarySignResponse } from '../wallets/signer';
import { createDefaultSignaturePrompter } from '../wallets/prompter';
import { buildCommitment, DEFAULT_SVC_CHALLENGE_COUNT, DEFAULT_SVC_MIN_CHUNKS_FOR_CHALLENGE } from './commitment';
import type { AvailabilityCommitment } from '../codegen/lumera/action/v1/metadata';

export type CascadeSignatureKind = "layout" | "index" | "auth";

Expand Down Expand Up @@ -237,7 +239,9 @@ export class CascadeUploader {
// Step 1: Get action params from blockchain
const actionParams = await this.chainPort.getActionParams();
const rq_ids_max = actionParams.max_raptor_q_symbols;
console.debug('CascadeUploader.registerAction actionParams', { actionParams });
const svcChallengeCount = actionParams.svc_challenge_count || DEFAULT_SVC_CHALLENGE_COUNT;
const svcMinChunks = actionParams.svc_min_chunks_for_challenge || DEFAULT_SVC_MIN_CHUNKS_FOR_CHALLENGE;
console.debug('CascadeUploader.registerAction actionParams', { actionParams, svcChallengeCount, svcMinChunks });

// Step 2: Generate random initial counter for layout ID derivation
const rq_ids_ic = Math.floor(Math.random() * rq_ids_max);
Expand Down Expand Up @@ -302,6 +306,18 @@ export class CascadeUploader {
const indexWithSignature = `${indexFileB64}.${indexSignatureResponse.signature}`;
console.debug('CascadeUploader.registerAction indexWithSignature', { indexWithSignature });

// Step 7b: Build LEP-5 availability commitment (Merkle tree over file chunks)
let availabilityCommitment: AvailabilityCommitment | undefined;
const commitmentResult = await buildCommitment(fileBytes, svcChallengeCount, svcMinChunks);
if (commitmentResult) {
availabilityCommitment = commitmentResult.commitment;
console.debug('CascadeUploader.registerAction built availability commitment', {
chunkSize: availabilityCommitment.chunkSize,
numChunks: availabilityCommitment.numChunks,
challengeIndices: availabilityCommitment.challengeIndices,
});
}

// Step 8: Prepare auth_signature for upload
const authSignatureResponse = await this.requestSignature(
"auth",
Expand All @@ -313,14 +329,35 @@ export class CascadeUploader {
console.debug('CascadeUploader.registerAction authSignature', { authSignature });

// Step 9: Register the action on-chain
const msg: Record<string, unknown> = {
data_hash: dataHash,
file_name: params.fileName,
rq_ids_ic,
signatures: indexWithSignature,
public: params.isPublic,
};
if (availabilityCommitment) {
msg.availability_commitment = {
commitment_type: availabilityCommitment.commitmentType,
hash_algo: availabilityCommitment.hashAlgo,
chunk_size: availabilityCommitment.chunkSize,
total_size: (() => {
const v = availabilityCommitment.totalSize;
if (typeof v === "bigint") {
if (v > BigInt(Number.MAX_SAFE_INTEGER)) {
throw new Error(`availability_commitment.total_size exceeds Number.MAX_SAFE_INTEGER: ${v.toString()}`);
}
return Number(v);
}
return v;
})(),
num_chunks: availabilityCommitment.numChunks,
root: Array.from(availabilityCommitment.root),
challenge_indices: availabilityCommitment.challengeIndices,
};
}
const txOutcome = await this.chainPort.requestActionTx({
msg: {
data_hash: dataHash,
file_name: params.fileName,
rq_ids_ic,
signatures: indexWithSignature,
public: params.isPublic,
},
msg,
expirationTime: params.expirationTime,
txPrompter: params.txPrompter,
}, fileBytes.length);
Expand Down
9 changes: 8 additions & 1 deletion src/internal/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -600,7 +600,14 @@ export class HttpClient {
* @returns Full URL
*/
private buildUrl(path: string, params?: Record<string, string | number | boolean>): string {
const url = new URL(path, this.config.baseUrl);
const base = new URL(this.config.baseUrl);
const baseHasPath = base.pathname !== '' && base.pathname !== '/';

// When baseUrl contains a path prefix (e.g. /proxy/snapi), absolute request
// paths like "/api/v1/..." would otherwise drop that prefix. Preserve it.
const resolvedPath = baseHasPath && path.startsWith('/') ? path.slice(1) : path;
const baseHref = this.config.baseUrl.endsWith('/') ? this.config.baseUrl : `${this.config.baseUrl}/`;
const url = new URL(resolvedPath, baseHref);

if (params) {
Object.entries(params).forEach(([key, value]) => {
Expand Down