-
Notifications
You must be signed in to change notification settings - Fork 16
Expand file tree
/
Copy pathuse-filecoin-upload.ts
More file actions
284 lines (255 loc) · 11.1 KB
/
use-filecoin-upload.ts
File metadata and controls
284 lines (255 loc) · 11.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
import { createCarFromFile } from 'filecoin-pin/core/unixfs'
import { checkUploadReadiness, executeUpload } from 'filecoin-pin/core/upload'
import pino from 'pino'
import { useCallback, useState } from 'react'
import { storeDataSetId, storeDataSetIdForProvider } from '../lib/local-storage/data-set.ts'
import type { StepState } from '../types/upload/step.ts'
import { getDebugParams } from '../utils/debug-params.ts'
import { formatFileSize } from '../utils/format-file-size.ts'
import { useFilecoinPinContext } from './use-filecoin-pin-context.ts'
import { cacheIpniResult } from './use-ipni-check.ts'
import { useWaitableRef } from './use-waitable-ref.ts'
interface UploadState {
isUploading: boolean
stepStates: StepState[]
error?: string
currentCid?: string
pieceCid?: string
transactionHash?: string
}
// Create a simple logger for the upload
const logger = pino({
level: 'debug',
browser: {
asObject: true,
},
})
export const INITIAL_STEP_STATES: StepState[] = [
{ step: 'creating-car', progress: 0, status: 'pending' },
{ step: 'checking-readiness', progress: 0, status: 'pending' },
{ step: 'uploading-car', progress: 0, status: 'pending' },
/**
* NOT GRANULAR.. only pending, in progress, completed
*
* This moves from pending to in-progress once the upload is completed.
* We then would want to verify that the CID is retrievable via IPNI before
* moving to completed.
*/
{ step: 'announcing-cids', progress: 0, status: 'pending' },
/**
* NOT GRANULAR.. only pending, in progress, completed
* This moves from pending to in-progress once the upload is completed.
* We then would want to verify that the transaction is on chain before moving to completed.
* in-progress->completed is confirmed by the onPieceConfirmed callback to `executeUpload`
*/
{ step: 'finalizing-transaction', progress: 0, status: 'pending' },
]
export const INPI_ERROR_MESSAGE =
"CID not yet indexed by IPNI. It's stored on Filecoin and fetchable now, but may take time to appear on IPFS."
/**
* Handles the end-to-end upload workflow with filecoin-pin:
* - Builds a CAR file in-browser
* - Checks upload readiness (allowances, balances)
* - Executes the upload with progress callbacks
* - Tracks IPNI availability and on-chain confirmation
*
* UI components receive a single `uploadState` object plus `uploadFile`/`resetUpload`
* actions so they stay dumb and declarative.
*/
export const useFilecoinUpload = () => {
const { synapse, storageContext, providerInfo, checkIfDatasetExists, wallet } = useFilecoinPinContext()
// Use waitable refs to track the latest context values, so the upload callback can access them
// even if the dataset is initialized after the callback is created
const storageContextRef = useWaitableRef(storageContext)
const providerInfoRef = useWaitableRef(providerInfo)
const synapseRef = useWaitableRef(synapse)
const [uploadState, setUploadState] = useState<UploadState>({
isUploading: false,
stepStates: INITIAL_STEP_STATES,
})
const updateStepState = useCallback((step: StepState['step'], updates: Partial<StepState>) => {
setUploadState((prev) => ({
...prev,
stepStates: prev.stepStates.map((stepState) =>
stepState.step === step ? { ...stepState, ...updates } : stepState
),
}))
}, [])
const uploadFile = useCallback(
async (file: File, metadata?: Record<string, string>): Promise<string> => {
setUploadState({
isUploading: true,
stepStates: INITIAL_STEP_STATES,
})
try {
// Step 1: Create CAR and upload to Filecoin SP
updateStepState('creating-car', { status: 'in-progress', progress: 0 })
logger.info('Creating CAR from file')
// Create CAR from file with progress tracking
const carResult = await createCarFromFile(file, {
onProgress: (bytesProcessed: number, totalBytes: number) => {
const progressPercent = Math.round((bytesProcessed / totalBytes) * 100)
updateStepState('creating-car', { progress: progressPercent })
},
})
// Store the CID for IPNI checking
const rootCid = carResult.rootCid.toString()
setUploadState((prev) => ({
...prev,
currentCid: rootCid,
}))
updateStepState('creating-car', { status: 'completed', progress: 100 })
logger.info({ carResult }, 'CAR created')
// creating the car is done, but its not uploaded yet.
// Step 2: Check readiness
updateStepState('checking-readiness', { status: 'in-progress', progress: 0 })
updateStepState('uploading-car', { status: 'in-progress', progress: 0 })
logger.info('Waiting for synapse to be initialized')
const synapse = await synapseRef.wait()
logger.info('Synapse initialized')
updateStepState('checking-readiness', { progress: 50 })
logger.info('Checking upload readiness')
// validate that we can actually upload the car, passing the autoConfigureAllowances flag to true to automatically configure allowances if needed.
const readinessCheck = await checkUploadReadiness({
synapse,
fileSize: carResult.carBytes.length,
autoConfigureAllowances: true,
})
logger.info({ readinessCheck }, 'Upload readiness check')
if (readinessCheck.status === 'blocked') {
// TODO: show the user the reasons why the upload is blocked, prompt them to fix based on the suggestions.
throw new Error('Readiness check failed')
}
updateStepState('checking-readiness', { status: 'completed', progress: 100 })
logger.info('Upload readiness check completed')
logger.info('Waiting for storage context and provider info to be initialized')
// Wait for storage context and provider info to be initialized
const [currentStorageContext, currentProviderInfo] = await Promise.all([
storageContextRef.wait(),
providerInfoRef.wait(),
])
logger.info('Storage context and provider info initialized')
// Capture the initial dataset ID (before upload) to detect if it's created during upload
const initialDataSetId = currentStorageContext.dataSetId
console.debug('[FilecoinUpload] Using storage context from provider:', {
providerInfo: currentProviderInfo,
dataSetId: initialDataSetId,
})
const synapseService = {
storage: currentStorageContext,
providerInfo: currentProviderInfo,
synapse,
}
// Step 3: Upload CAR to Synapse (Filecoin SP)
logger.info('Uploading CAR to Synapse')
await executeUpload(synapseService, carResult.carBytes, carResult.rootCid, {
logger,
contextId: `upload-${Date.now()}`,
metadata: {
...(metadata ?? {}),
label: file.name,
fileSize: formatFileSize(file.size),
},
onProgress: (event) => {
switch (event.type) {
case 'onUploadComplete':
console.debug('[FilecoinUpload] Upload complete, piece CID:', event.data.pieceCid)
// Store the piece CID from the callback
setUploadState((prev) => ({
...prev,
pieceCid: event.data.pieceCid.toString(),
}))
updateStepState('uploading-car', { status: 'completed', progress: 100 })
// now the other steps can move to in-progress
updateStepState('announcing-cids', { status: 'in-progress', progress: 0 })
break
case 'onPieceAdded': {
const txHash = event.data.txHash
console.debug('[FilecoinUpload] Piece add transaction:', { txHash })
// Store the transaction hash if available
if (txHash) {
setUploadState((prev) => ({
...prev,
transactionHash: txHash,
}))
}
// now the finalizing-transaction step can move to in-progress
updateStepState('finalizing-transaction', { status: 'in-progress', progress: 0 })
break
}
case 'onPieceConfirmed': {
// Save the dataset ID if it was just created during this upload
const currentDataSetId = currentStorageContext.dataSetId
if (wallet?.status === 'ready' && currentDataSetId !== undefined && initialDataSetId === undefined) {
const debugParams = getDebugParams()
// Only use storeDataSetIdForProvider if user explicitly provided providerId in URL
if (debugParams.providerId !== null) {
storeDataSetIdForProvider(wallet.data.address, currentProviderInfo.id, currentDataSetId)
} else {
storeDataSetId(wallet.data.address, currentDataSetId)
}
}
// Complete finalization
updateStepState('finalizing-transaction', { status: 'completed', progress: 100 })
console.debug('[FilecoinUpload] Upload fully completed and confirmed on chain')
break
}
case 'ipniProviderResults.failed': {
// IPNI check failed - mark as error with a helpful message
console.warn('[FilecoinUpload] IPNI check failed after max attempts:', event.data.error.message)
// Cache the failed result
cacheIpniResult(rootCid, 'failed')
updateStepState('announcing-cids', {
status: 'error',
progress: 0,
error: INPI_ERROR_MESSAGE,
})
break
}
case 'ipniProviderResults.complete': {
console.debug('[FilecoinUpload] IPNI check succeeded, marking announcing-cids as completed')
// Cache the success result
cacheIpniResult(rootCid, 'success')
updateStepState('announcing-cids', { status: 'completed', progress: 100 })
break
}
default:
break
}
},
})
logger.info('Upload completed')
// Return the actual CID from the CAR result
return rootCid
} catch (error) {
console.error('[FilecoinUpload] Upload failed with error:', error)
const errorMessage = error instanceof Error ? error.message : 'Upload failed'
setUploadState((prev) => ({
...prev,
error: errorMessage,
}))
throw error
} finally {
setUploadState((prev) => ({
...prev,
isUploading: false,
}))
}
},
[updateStepState, synapse, checkIfDatasetExists]
)
const resetUpload = useCallback(() => {
setUploadState({
isUploading: false,
stepStates: INITIAL_STEP_STATES,
currentCid: undefined,
pieceCid: undefined,
transactionHash: undefined,
})
}, [])
return {
uploadState,
uploadFile,
resetUpload,
}
}