Skip to content

Commit 91b5628

Browse files
committed
feat: check payments status on import command
1 parent 4985d57 commit 91b5628

File tree

3 files changed

+226
-2
lines changed

3 files changed

+226
-2
lines changed

src/import/car-import.ts

Lines changed: 96 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import { CarReader } from '@ipld/car'
1212
import { CID } from 'multiformats/cid'
1313
import pc from 'picocolors'
1414
import pino from 'pino'
15+
import { checkInsufficientFunds, formatUSDFC } from '../payments/setup.js'
16+
import { checkFILBalance, checkUSDFCBalance, validatePaymentCapacity } from '../synapse/payments.js'
1517
import { cleanupSynapseService, initializeSynapse } from '../synapse/service.js'
1618
import { uploadToSynapse } from '../synapse/upload.js'
1719
import { createSpinner, formatFileSize, intro } from '../utils/cli-helpers.js'
@@ -166,8 +168,100 @@ export async function runCarImport(options: ImportOptions): Promise<ImportResult
166168

167169
spinner.stop(`${pc.green('✓')} Connected to ${pc.bold(network)}`)
168170

169-
// Step 5: Read CAR file and upload to Synapse
170-
spinner.start('Uploading to Filecoin...')
171+
// Step 5: Validate payment setup
172+
spinner.start('Validating payment setup...')
173+
174+
// First check basic requirements (FIL and USDFC balance)
175+
const { isCalibnet, hasSufficientGas } = await checkFILBalance(synapseService.synapse)
176+
const usdfcBalance = await checkUSDFCBalance(synapseService.synapse)
177+
178+
// Check basic requirements using existing function
179+
const hasBasicRequirements = checkInsufficientFunds(hasSufficientGas, usdfcBalance, isCalibnet, false)
180+
181+
if (!hasBasicRequirements) {
182+
spinner.stop(`${pc.red('✗')} Payment setup incomplete`)
183+
log.line('')
184+
log.line(`${pc.yellow('⚠')} Your payment setup is not complete. Please run:`)
185+
log.indent(pc.cyan('filecoin-pin payments setup'))
186+
log.line('')
187+
log.line('For more information, run:')
188+
log.indent(pc.cyan('filecoin-pin payments status'))
189+
log.flush()
190+
await cleanupSynapseService()
191+
process.exit(1)
192+
}
193+
194+
// Now check capacity for this specific file
195+
const capacityCheck = await validatePaymentCapacity(synapseService.synapse, fileStat.size)
196+
197+
if (!capacityCheck.canUpload) {
198+
spinner.stop(`${pc.red('✗')} Insufficient payment capacity for this file`)
199+
log.line('')
200+
log.line(pc.bold('File Requirements:'))
201+
log.indent(`File size: ${formatFileSize(fileStat.size)} (${capacityCheck.storageTiB.toFixed(4)} TiB)`)
202+
log.indent(`Storage cost: ${formatUSDFC(capacityCheck.required.rateAllowance)} USDFC/epoch`)
203+
log.indent(`10-day lockup: ${formatUSDFC(capacityCheck.required.lockupAllowance)} USDFC`)
204+
log.line('')
205+
206+
log.line(pc.bold(`${pc.red('Issues found:')}`))
207+
if (capacityCheck.issues.insufficientDeposit) {
208+
log.indent(
209+
`${pc.red('✗')} Insufficient deposit (need ${formatUSDFC(capacityCheck.issues.insufficientDeposit)} more)`
210+
)
211+
}
212+
if (capacityCheck.issues.insufficientRateAllowance) {
213+
log.indent(
214+
`${pc.red('✗')} Rate allowance too low (need ${formatUSDFC(capacityCheck.issues.insufficientRateAllowance)} more per epoch)`
215+
)
216+
}
217+
if (capacityCheck.issues.insufficientLockupAllowance) {
218+
log.indent(
219+
`${pc.red('✗')} Lockup allowance too low (need ${formatUSDFC(capacityCheck.issues.insufficientLockupAllowance)} more)`
220+
)
221+
}
222+
log.line('')
223+
224+
log.line(pc.bold('Suggested actions:'))
225+
capacityCheck.suggestions.forEach((suggestion) => {
226+
log.indent(`• ${suggestion}`)
227+
})
228+
log.line('')
229+
230+
// Calculate suggested parameters for payment setup
231+
const suggestedDeposit = capacityCheck.issues.insufficientDeposit
232+
? formatUSDFC(capacityCheck.issues.insufficientDeposit)
233+
: '0'
234+
const suggestedStorage = `${Math.ceil(capacityCheck.storageTiB * 10) / 10}TiB/month`
235+
236+
log.line(`${pc.yellow('⚠')} To fix these issues, run:`)
237+
if (capacityCheck.issues.insufficientDeposit) {
238+
log.indent(
239+
pc.cyan(`filecoin-pin payments setup --deposit ${suggestedDeposit} --storage ${suggestedStorage} --auto`)
240+
)
241+
} else {
242+
log.indent(pc.cyan(`filecoin-pin payments setup --storage ${suggestedStorage} --auto`))
243+
}
244+
log.flush()
245+
await cleanupSynapseService()
246+
process.exit(1)
247+
}
248+
249+
// Show warning if suggestions exist (even if upload is possible)
250+
if (capacityCheck.suggestions.length > 0 && capacityCheck.canUpload) {
251+
spinner.stop(`${pc.yellow('⚠')} Payment capacity check passed with warnings`)
252+
log.line('')
253+
log.line(pc.bold('Suggestions:'))
254+
capacityCheck.suggestions.forEach((suggestion) => {
255+
log.indent(`• ${suggestion}`)
256+
})
257+
log.flush()
258+
spinner.start('Uploading to Filecoin...')
259+
} else {
260+
spinner.stop(`${pc.green('✓')} Payment capacity verified`)
261+
spinner.start('Uploading to Filecoin...')
262+
}
263+
264+
// Step 6: Read CAR file and upload to Synapse
171265

172266
// Read the entire CAR file (streaming not yet supported in Synapse)
173267
const carData = await readFile(options.filePath)

src/synapse/payments.ts

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,3 +345,110 @@ export function calculateStorageFromUSDFC(usdfcAmount: bigint, pricePerTiBPerEpo
345345
const capacityTiB = Number((ratePerEpoch * 100n) / pricePerTiBPerEpoch) / 100
346346
return capacityTiB
347347
}
348+
349+
/**
350+
* Calculate required allowances from CAR file size
351+
*
352+
* Simple wrapper that converts file size to storage allowances.
353+
*
354+
* @param carSizeBytes - Size of the CAR file in bytes
355+
* @param pricePerTiBPerEpoch - Current pricing from storage service
356+
* @returns Required allowances for the file
357+
*/
358+
export function calculateRequiredAllowances(carSizeBytes: number, pricePerTiBPerEpoch: bigint): StorageAllowances {
359+
// Convert bytes to TiB (1 TiB = 1024^4 bytes)
360+
const bytesPerTiB = 1024 * 1024 * 1024 * 1024
361+
const storageTiB = carSizeBytes / bytesPerTiB
362+
return calculateStorageAllowances(storageTiB, pricePerTiBPerEpoch)
363+
}
364+
365+
/**
366+
* Payment capacity validation for a specific file
367+
*/
368+
export interface PaymentCapacityCheck {
369+
canUpload: boolean
370+
storageTiB: number
371+
required: StorageAllowances
372+
issues: {
373+
insufficientDeposit?: bigint
374+
insufficientRateAllowance?: bigint
375+
insufficientLockupAllowance?: bigint
376+
}
377+
suggestions: string[]
378+
}
379+
380+
/**
381+
* Validate payment capacity for a specific CAR file
382+
*
383+
* Checks if the current payment setup can handle uploading a specific file.
384+
* This is a focused check on capacity, not basic setup validation.
385+
*
386+
* Example usage:
387+
* ```typescript
388+
* const fileSize = 10 * 1024 * 1024 * 1024 // 10 GiB
389+
* const capacity = await validatePaymentCapacity(synapse, fileSize)
390+
*
391+
* if (!capacity.canUpload) {
392+
* console.error('Cannot upload file with current payment setup')
393+
* capacity.suggestions.forEach(s => console.log(` - ${s}`))
394+
* }
395+
* ```
396+
*
397+
* @param synapse - Initialized Synapse instance
398+
* @param carSizeBytes - Size of the CAR file in bytes
399+
* @returns Capacity check result with specific issues
400+
*/
401+
export async function validatePaymentCapacity(synapse: Synapse, carSizeBytes: number): Promise<PaymentCapacityCheck> {
402+
// Get current status and pricing
403+
const [status, storageInfo] = await Promise.all([getPaymentStatus(synapse), synapse.storage.getStorageInfo()])
404+
405+
const pricePerTiBPerEpoch = storageInfo.pricing.noCDN.perTiBPerEpoch
406+
const bytesPerTiB = 1024 * 1024 * 1024 * 1024
407+
const storageTiB = carSizeBytes / bytesPerTiB
408+
409+
// Calculate requirements
410+
const required = calculateRequiredAllowances(carSizeBytes, pricePerTiBPerEpoch)
411+
const monthlyPayment = required.rateAllowance * TIME_CONSTANTS.EPOCHS_PER_MONTH
412+
const totalDepositNeeded = required.lockupAllowance + monthlyPayment
413+
414+
const result: PaymentCapacityCheck = {
415+
canUpload: true,
416+
storageTiB,
417+
required,
418+
issues: {},
419+
suggestions: [],
420+
}
421+
422+
// Check deposit
423+
if (status.depositedAmount < totalDepositNeeded) {
424+
result.canUpload = false
425+
result.issues.insufficientDeposit = totalDepositNeeded - status.depositedAmount
426+
const depositNeeded = ethers.formatUnits(totalDepositNeeded - status.depositedAmount, 18)
427+
result.suggestions.push(`Deposit at least ${depositNeeded} USDFC`)
428+
}
429+
430+
// Check rate allowance
431+
if (status.currentAllowances.rateAllowance < required.rateAllowance) {
432+
result.canUpload = false
433+
result.issues.insufficientRateAllowance = required.rateAllowance - status.currentAllowances.rateAllowance
434+
const rateNeeded = ethers.formatUnits(required.rateAllowance, 18)
435+
result.suggestions.push(`Set rate allowance to at least ${rateNeeded} USDFC/epoch`)
436+
}
437+
438+
// Check lockup allowance
439+
if (status.currentAllowances.lockupAllowance < required.lockupAllowance) {
440+
result.canUpload = false
441+
result.issues.insufficientLockupAllowance = required.lockupAllowance - status.currentAllowances.lockupAllowance
442+
const lockupNeeded = ethers.formatUnits(required.lockupAllowance, 18)
443+
result.suggestions.push(`Set lockup allowance to at least ${lockupNeeded} USDFC`)
444+
}
445+
446+
// Add warning if approaching deposit limit
447+
const totalLockupAfter = status.currentAllowances.lockupUsed + required.lockupAllowance
448+
if (totalLockupAfter > (status.depositedAmount * 9n) / 10n && result.canUpload) {
449+
const additionalDeposit = ethers.formatUnits((totalLockupAfter * 11n) / 10n - status.depositedAmount, 18)
450+
result.suggestions.push(`Consider depositing ${additionalDeposit} more USDFC for safety margin`)
451+
}
452+
453+
return result
454+
}

src/test/unit/car-import.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,29 @@ import type { ImportOptions } from '../../import/types.js'
2323

2424
// Mock modules
2525
vi.mock('@filoz/synapse-sdk', async () => await import('../mocks/synapse-sdk.js'))
26+
vi.mock('../../synapse/payments.js', () => ({
27+
checkFILBalance: vi.fn().mockResolvedValue({
28+
balance: 1000000000000000000n, // 1 FIL
29+
isCalibnet: true,
30+
hasSufficientGas: true,
31+
}),
32+
checkUSDFCBalance: vi.fn().mockResolvedValue(1000000000000000000000n), // 1000 USDFC
33+
validatePaymentCapacity: vi.fn().mockResolvedValue({
34+
canUpload: true,
35+
storageTiB: 0.001,
36+
required: {
37+
rateAllowance: 100000000000000n,
38+
lockupAllowance: 1000000000000000000n,
39+
storageCapacityTiB: 0.001,
40+
},
41+
issues: {},
42+
suggestions: [],
43+
}),
44+
}))
45+
vi.mock('../../payments/setup.js', () => ({
46+
checkInsufficientFunds: vi.fn().mockReturnValue(true),
47+
formatUSDFC: vi.fn((amount) => `${amount} USDFC`),
48+
}))
2649
vi.mock('../../synapse/service.js', async () => {
2750
const { MockSynapse } = await import('../mocks/synapse-mocks.js')
2851

0 commit comments

Comments
 (0)