Skip to content

Commit 9ab3f8c

Browse files
SgtPookiBigLepCopilot
authored
feat: add more details to upload-flow (#169)
* fix: add verbose cli decorator * fix: use onProgress fn in filecoin-pin * fix: add ipni-announcement-check * feat: more details during add/import * fix: better UI during upload-flow * Adjusted wording * Updated gitignore to exclude .env * fix: check ipni advertisement during upload * fix: executeUpload only awaits ipniValidationPromise if it exists * test: validateIPNIadvertisement returns true in import.test * fix: better onProgressTypes * chore: enable strictFucntionTypes tsc option * Update tsconfig.json * Update src/test/unit/import.test.ts * chore: fix lint * Update src/core/upload/index.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * fix: remove unused var and if block * Update src/utils/cli-logger.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/core/upload/index.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Steve Loeppky <stvn@loeppky.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent b020a42 commit 9ab3f8c

File tree

10 files changed

+253
-88
lines changed

10 files changed

+253
-88
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
.env
12
node_modules/
23
package-lock.json
34
boxo/

src/add/add.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
import { cleanupTempCar, createCarFromPath } from '../core/unixfs/index.js'
2020
import { parseCLIAuth, parseProviderOptions } from '../utils/cli-auth.js'
2121
import { cancel, createSpinner, formatFileSize, intro, outro } from '../utils/cli-helpers.js'
22+
import { log } from '../utils/cli-logger.js'
2223
import type { AddOptions, AddResult } from './types.js'
2324

2425
/**
@@ -175,6 +176,10 @@ export async function runAdd(options: AddOptions): Promise<AddResult> {
175176
})
176177

177178
spinner.stop(`${pc.green('✓')} Storage context ready`)
179+
log.spinnerSection('Storage Context', [
180+
pc.gray(`Data Set ID: ${storage.dataSetId}`),
181+
pc.gray(`Provider: ${providerInfo.name || providerInfo.serviceProvider}`),
182+
])
178183

179184
// Create service object for upload function
180185
const synapseService: SynapseService = { synapse, storage, providerInfo }

src/common/upload-flow.ts

Lines changed: 120 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,19 @@
55
* including payment validation, storage context creation, and result display.
66
*/
77

8-
import type { Synapse } from '@filoz/synapse-sdk'
8+
import type { PieceCID, Synapse } from '@filoz/synapse-sdk'
99
import type { CID } from 'multiformats/cid'
1010
import pc from 'picocolors'
1111
import type { Logger } from 'pino'
1212
import { DEFAULT_LOCKUP_DAYS, type PaymentCapacityCheck } from '../core/payments/index.js'
1313
import { cleanupSynapseService, type SynapseService } from '../core/synapse/index.js'
14-
import { checkUploadReadiness, executeUpload, getDownloadURL, type SynapseUploadResult } from '../core/upload/index.js'
14+
import {
15+
checkUploadReadiness,
16+
executeUpload,
17+
getDownloadURL,
18+
getServiceURL,
19+
type SynapseUploadResult,
20+
} from '../core/upload/index.js'
1521
import { formatUSDFC } from '../core/utils/format.js'
1622
import { autoFund } from '../payments/fund.js'
1723
import type { AutoFundOptions } from '../payments/types.js'
@@ -245,19 +251,122 @@ export async function performUpload(
245251

246252
spinner?.start('Uploading to Filecoin...')
247253

254+
// Track parallel operations with their messages
255+
const pendingOps = new Map<string, string>()
256+
let transactionHash: string | undefined
257+
258+
function getSpinnerMessage() {
259+
return Array.from(pendingOps.values())
260+
.map((op) => op)
261+
.join(' & ')
262+
}
263+
264+
function completeOperation(
265+
operationKey: string,
266+
completionMessage: string,
267+
type: 'success' | 'warning' | 'info' | 'none' = 'success'
268+
) {
269+
pendingOps.delete(operationKey)
270+
271+
switch (type) {
272+
case 'success':
273+
spinner?.stop(`${pc.green('✓')} ${completionMessage}`)
274+
break
275+
case 'warning':
276+
spinner?.stop(`${pc.yellow('⚠')} ${completionMessage}`)
277+
break
278+
default:
279+
spinner?.stop(completionMessage)
280+
break
281+
}
282+
283+
// Restart spinner with remaining operations if any
284+
if (pendingOps.size > 0) {
285+
spinner?.start(getSpinnerMessage())
286+
}
287+
}
288+
289+
let pieceCid: PieceCID | undefined
290+
function getIpniAdvertisementMsg(attemptCount: number): string {
291+
return `Checking for IPNI advertisement (check #${attemptCount})`
292+
}
293+
248294
const uploadResult = await executeUpload(synapseService, carData, rootCid, {
249295
logger,
250296
contextId: `${contextType}-${Date.now()}`,
251-
ipniValidation: { enabled: false },
252-
callbacks: {
253-
onUploadComplete: () => {
254-
spinner?.message('Upload complete, adding to data set...')
255-
},
256-
onPieceAdded: (transaction) => {
257-
if (transaction) {
258-
spinner?.message('Piece added to data set, confirming on-chain...')
297+
onProgress(event) {
298+
switch (event.type) {
299+
case 'onUploadComplete': {
300+
pieceCid = event.data.pieceCid
301+
spinner?.stop(`${pc.green('✓')} Upload complete`)
302+
const serviceURL = getServiceURL(synapseService.providerInfo)
303+
if (serviceURL != null && serviceURL !== '') {
304+
log.spinnerSection('Download IPFS CAR from SP', [
305+
pc.gray(`${serviceURL.replace(/\/$/, '')}/ipfs/${rootCid}`),
306+
])
307+
}
308+
spinner?.start('Adding piece to DataSet...')
309+
break
310+
}
311+
case 'onPieceAdded': {
312+
spinner?.stop(`${pc.green('✓')} Piece added to DataSet (unconfirmed on-chain)`)
313+
if (event.data.txHash) {
314+
transactionHash = event.data.txHash
315+
}
316+
log.spinnerSection('Explorer URLs', [
317+
pc.gray(`Piece: https://pdp.vxb.ai/calibration/piece/${pieceCid}`),
318+
pc.gray(
319+
`Transaction: https://${synapseService.synapse.getNetwork()}.filfox.info/en/message/${transactionHash}`
320+
),
321+
])
322+
323+
pendingOps.set('chain', 'Confirming piece added to DataSet on-chain')
324+
325+
spinner?.start(getSpinnerMessage())
326+
break
327+
}
328+
case 'onPieceConfirmed': {
329+
completeOperation('chain', `Piece added to DataSet (confirmed on-chain)`, 'success')
330+
break
331+
}
332+
333+
case 'ipniAdvertisement.retryUpdate': {
334+
if (event.data.retryCount === 0) {
335+
pendingOps.set('ipni', getIpniAdvertisementMsg(1))
336+
}
337+
pendingOps.set('ipni', getIpniAdvertisementMsg(event.data.retryCount + 1))
338+
spinner?.message(getSpinnerMessage())
339+
break
259340
}
260-
},
341+
case 'ipniAdvertisement.complete': {
342+
const isIpniAdvertisementSuccessful = event.data.result
343+
const message = isIpniAdvertisementSuccessful
344+
? `IPNI advertisement successful. IPFS retrieval possible.`
345+
: `IPNI advertisement pending`
346+
347+
completeOperation('ipni', message, isIpniAdvertisementSuccessful ? 'success' : 'warning')
348+
349+
if (isIpniAdvertisementSuccessful) {
350+
log.spinnerSection('IPFS Retrieval URLs', [
351+
pc.gray(`ipfs://${rootCid}`),
352+
pc.gray(`https://inbrowser.link/ipfs/${rootCid}`),
353+
pc.gray(`https://dweb.link/ipfs/${rootCid}`),
354+
])
355+
}
356+
break
357+
}
358+
case 'ipniAdvertisement.failed': {
359+
logger.error({ error: event.data.error }, 'Error checking IPNI advertisement')
360+
completeOperation('ipni', `IPNI advertisement check failed`, 'warning')
361+
log.spinnerSection('IPNI advertisement check failed', [
362+
pc.gray(`IPNI advertisement does not exist at http://filecoinpin.contact/cid/${rootCid}`),
363+
])
364+
break
365+
}
366+
default: {
367+
break
368+
}
369+
}
261370
},
262371
})
263372

src/core/upload/index.ts

Lines changed: 41 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { Synapse, UploadCallbacks } from '@filoz/synapse-sdk'
1+
import type { Synapse } from '@filoz/synapse-sdk'
22
import type { CID } from 'multiformats/cid'
33
import type { Logger } from 'pino'
44
import {
@@ -11,24 +11,26 @@ import {
1111
validatePaymentRequirements,
1212
} from '../payments/index.js'
1313
import { isSessionKeyMode, type SynapseService } from '../synapse/index.js'
14+
import type { ProgressEvent, ProgressEventHandler } from '../utils/types.js'
1415
import {
1516
type ValidateIPNIAdvertisementOptions,
17+
type ValidateIPNIProgressEvents,
1618
validateIPNIAdvertisement,
1719
} from '../utils/validate-ipni-advertisement.js'
18-
import { type SynapseUploadResult, uploadToSynapse } from './synapse.js'
20+
import { type SynapseUploadResult, type UploadProgressEvents, uploadToSynapse } from './synapse.js'
1921

20-
export type { SynapseUploadOptions, SynapseUploadResult } from './synapse.js'
22+
export type { SynapseUploadOptions, SynapseUploadResult, UploadProgressEvents } from './synapse.js'
2123
export { getDownloadURL, getServiceURL, uploadToSynapse } from './synapse.js'
2224

2325
/**
2426
* Options for evaluating whether an upload can proceed.
2527
*/
26-
export type UploadReadinessProgressEvent =
27-
| { type: 'checking-balances' }
28-
| { type: 'checking-allowances' }
29-
| { type: 'configuring-allowances' }
30-
| { type: 'allowances-configured'; transactionHash?: string }
31-
| { type: 'validating-capacity' }
28+
export type UploadReadinessProgressEvents =
29+
| ProgressEvent<'checking-balances'>
30+
| ProgressEvent<'checking-allowances'>
31+
| ProgressEvent<'configuring-allowances'>
32+
| ProgressEvent<'allowances-configured', { transactionHash?: string }>
33+
| ProgressEvent<'validating-capacity'>
3234

3335
export interface UploadReadinessOptions {
3436
/** Initialized Synapse instance. */
@@ -41,7 +43,7 @@ export interface UploadReadinessOptions {
4143
*/
4244
autoConfigureAllowances?: boolean
4345
/** Optional callback for progress updates. */
44-
onProgress?: (event: UploadReadinessProgressEvent) => void
46+
onProgress?: ProgressEventHandler<UploadReadinessProgressEvents>
4547
}
4648

4749
/**
@@ -129,7 +131,7 @@ export async function checkUploadReadiness(options: UploadReadinessOptions): Pro
129131
const setResult = await setMaxAllowances(synapse)
130132
allowancesUpdated = true
131133
allowanceTxHash = setResult.transactionHash
132-
onProgress?.({ type: 'allowances-configured', transactionHash: allowanceTxHash })
134+
onProgress?.({ type: 'allowances-configured', data: { transactionHash: allowanceTxHash } })
133135
}
134136

135137
onProgress?.({ type: 'validating-capacity' })
@@ -179,8 +181,8 @@ export interface UploadExecutionOptions {
179181
logger: Logger
180182
/** Optional identifier to help correlate logs. */
181183
contextId?: string
182-
/** Optional callbacks mirroring Synapse SDK upload callbacks. */
183-
callbacks?: UploadCallbacks
184+
/** Optional umbrella onProgress receiving child progress events. */
185+
onProgress?: ProgressEventHandler<(UploadProgressEvents | ValidateIPNIProgressEvents) & {}>
184186
/** Optional metadata to associate with the upload. */
185187
metadata?: Record<string, string>
186188
/**
@@ -193,7 +195,7 @@ export interface UploadExecutionOptions {
193195
* @default: true
194196
*/
195197
enabled?: boolean
196-
} & ValidateIPNIAdvertisementOptions
198+
} & Omit<ValidateIPNIAdvertisementOptions, 'onProgress'>
197199
}
198200

199201
export interface UploadExecutionResult extends SynapseUploadResult {
@@ -219,40 +221,40 @@ export async function executeUpload(
219221
rootCid: CID,
220222
options: UploadExecutionOptions
221223
): Promise<UploadExecutionResult> {
222-
const { logger, contextId, callbacks } = options
224+
const { logger, contextId } = options
223225
let transactionHash: string | undefined
224226
let ipniValidationPromise: Promise<boolean> | undefined
225227

226-
const mergedCallbacks: UploadCallbacks = {
227-
onUploadComplete: (pieceCid) => {
228-
callbacks?.onUploadComplete?.(pieceCid)
229-
// Begin IPNI validation as soon as the upload completes
230-
if (options.ipniValidation?.enabled !== false && ipniValidationPromise == null) {
231-
try {
232-
const { enabled: _enabled, ...rest } = options.ipniValidation ?? {}
233-
ipniValidationPromise = validateIPNIAdvertisement(rootCid, {
234-
...rest,
235-
logger,
236-
})
237-
} catch (error) {
238-
logger.error({ error }, 'Could not begin IPNI advertisement validation')
239-
ipniValidationPromise = Promise.resolve(false)
228+
const onProgress: ProgressEventHandler<UploadProgressEvents | ValidateIPNIProgressEvents> = (event) => {
229+
switch (event.type) {
230+
case 'onPieceAdded': {
231+
// Begin IPNI validation as soon as the piece is added and parked in the data set
232+
if (options.ipniValidation?.enabled !== false && ipniValidationPromise == null) {
233+
try {
234+
const { enabled: _enabled, ...rest } = options.ipniValidation ?? {}
235+
ipniValidationPromise = validateIPNIAdvertisement(rootCid, {
236+
...rest,
237+
logger,
238+
})
239+
} catch (error) {
240+
logger.error({ error }, 'Could not begin IPNI advertisement validation')
241+
ipniValidationPromise = Promise.resolve(false)
242+
}
240243
}
244+
if (event.data.txHash != null) {
245+
transactionHash = event.data.txHash
246+
}
247+
break
241248
}
242-
},
243-
onPieceAdded: (txHash) => {
244-
if (txHash) {
245-
transactionHash = txHash
249+
default: {
250+
break
246251
}
247-
callbacks?.onPieceAdded?.(txHash)
248-
},
249-
onPieceConfirmed: (pieceIds) => {
250-
callbacks?.onPieceConfirmed?.(pieceIds)
251-
},
252+
}
253+
options.onProgress?.(event)
252254
}
253255

254256
const uploadOptions: Parameters<typeof uploadToSynapse>[4] = {
255-
callbacks: mergedCallbacks,
257+
onProgress,
256258
}
257259
if (contextId) {
258260
uploadOptions.contextId = contextId

src/core/upload/synapse.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,23 @@
44
* This module provides a reusable upload pattern for CAR files to Filecoin
55
* via Synapse SDK, used by both the import command and pinning server.
66
*/
7-
import type { UploadOptions } from '@filoz/synapse-sdk'
7+
import type { PieceCID, UploadOptions } from '@filoz/synapse-sdk'
88
import { METADATA_KEYS, type ProviderInfo, type UploadCallbacks } from '@filoz/synapse-sdk'
99
import type { CID } from 'multiformats/cid'
1010
import type { Logger } from 'pino'
1111
import type { SynapseService } from '../synapse/index.js'
12+
import type { ProgressEvent, ProgressEventHandler } from '../utils/types.js'
13+
14+
export type UploadProgressEvents =
15+
| ProgressEvent<'onUploadComplete', { pieceCid: PieceCID }>
16+
| ProgressEvent<'onPieceAdded', { txHash: `0x${string}` | undefined }>
17+
| ProgressEvent<'onPieceConfirmed', { pieceIds: number[] }>
1218

1319
export interface SynapseUploadOptions {
1420
/**
1521
* Optional callbacks for monitoring upload progress
1622
*/
17-
callbacks?: UploadCallbacks
23+
onProgress?: ProgressEventHandler<UploadProgressEvents>
1824

1925
/**
2026
* Context identifier for logging (e.g., pinId, import job ID)
@@ -71,7 +77,7 @@ export async function uploadToSynapse(
7177
logger: Logger,
7278
options: SynapseUploadOptions = {}
7379
): Promise<SynapseUploadResult> {
74-
const { callbacks, contextId = 'upload' } = options
80+
const { onProgress, contextId = 'upload' } = options
7581

7682
// Merge provided callbacks with logging callbacks
7783
const uploadCallbacks: UploadCallbacks = {
@@ -84,7 +90,7 @@ export async function uploadToSynapse(
8490
},
8591
'Upload to PDP server complete'
8692
)
87-
callbacks?.onUploadComplete?.(pieceCid)
93+
onProgress?.({ type: 'onUploadComplete', data: { pieceCid } })
8894
},
8995

9096
onPieceAdded: (txHash) => {
@@ -106,7 +112,7 @@ export async function uploadToSynapse(
106112
'Piece added to data set'
107113
)
108114
}
109-
callbacks?.onPieceAdded?.(txHash)
115+
onProgress?.({ type: 'onPieceAdded', data: { txHash } })
110116
},
111117

112118
onPieceConfirmed: (pieceIds) => {
@@ -118,7 +124,7 @@ export async function uploadToSynapse(
118124
},
119125
'Piece addition confirmed on-chain'
120126
)
121-
callbacks?.onPieceConfirmed?.(pieceIds)
127+
onProgress?.({ type: 'onPieceConfirmed', data: { pieceIds } })
122128
},
123129
}
124130

src/core/utils/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export * from './format.js'
2+
export * from './types.js'
23
export * from './validate-ipni-advertisement.js'

src/test/unit/import.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,9 @@ vi.mock('../../core/payments/index.js', async () => {
7676
}),
7777
}
7878
})
79+
vi.mock('../../core/utils/validate-ipni-advertisement.js', () => ({
80+
validateIPNIAdvertisement: vi.fn().mockResolvedValue(true),
81+
}))
7982

8083
vi.mock('../../payments/setup.js', () => ({
8184
formatUSDFC: vi.fn((amount) => `${amount} USDFC`),

0 commit comments

Comments
 (0)