diff --git a/packages/synapse-core/src/mocks/jsonrpc/index.ts b/packages/synapse-core/src/mocks/jsonrpc/index.ts index c3fa13832..ff223f2b7 100644 --- a/packages/synapse-core/src/mocks/jsonrpc/index.ts +++ b/packages/synapse-core/src/mocks/jsonrpc/index.ts @@ -522,6 +522,7 @@ export const presets = { getDataSetStorageProvider: () => [ADDRESSES.serviceProvider1, ADDRESSES.zero], getDataSetLeafCount: () => [0n], getScheduledRemovals: () => [[]], + getNextChallengeEpoch: () => [5000n], }, serviceRegistry: { registerProvider: () => [1n], diff --git a/packages/synapse-core/src/mocks/jsonrpc/pdp.ts b/packages/synapse-core/src/mocks/jsonrpc/pdp.ts index c59afc29f..f0eac032f 100644 --- a/packages/synapse-core/src/mocks/jsonrpc/pdp.ts +++ b/packages/synapse-core/src/mocks/jsonrpc/pdp.ts @@ -13,6 +13,7 @@ export type getActivePieces = ExtractAbiFunction export type getDataSetLeafCount = ExtractAbiFunction export type getScheduledRemovals = ExtractAbiFunction +export type getNextChallengeEpoch = ExtractAbiFunction export interface PDPVerifierOptions { dataSetLive?: (args: AbiToType) => AbiToType @@ -25,6 +26,9 @@ export interface PDPVerifierOptions { ) => AbiToType getDataSetLeafCount?: (args: AbiToType) => AbiToType getScheduledRemovals?: (args: AbiToType) => AbiToType + getNextChallengeEpoch?: ( + args: AbiToType + ) => AbiToType } /** @@ -111,6 +115,15 @@ export function pdpVerifierCallHandler(data: Hex, options: JSONRPCOptions): Hex options.pdpVerifier.getScheduledRemovals(args) ) } + case 'getNextChallengeEpoch': { + if (!options.pdpVerifier?.getNextChallengeEpoch) { + throw new Error('PDP Verifier: getNextChallengeEpoch is not defined') + } + return encodeAbiParameters( + Abis.pdp.find((abi) => abi.type === 'function' && abi.name === 'getNextChallengeEpoch')!.outputs, + options.pdpVerifier.getNextChallengeEpoch(args) + ) + } default: { throw new Error(`PDP Verifier: unknown function: ${functionName} with args: ${args}`) } diff --git a/packages/synapse-core/src/pdp-verifier/get-next-challenge-epoch.ts b/packages/synapse-core/src/pdp-verifier/get-next-challenge-epoch.ts new file mode 100644 index 000000000..1575364e1 --- /dev/null +++ b/packages/synapse-core/src/pdp-verifier/get-next-challenge-epoch.ts @@ -0,0 +1,117 @@ +import type { Simplify } from 'type-fest' +import type { + Address, + Chain, + Client, + ContractFunctionParameters, + ContractFunctionReturnType, + ReadContractErrorType, + Transport, +} from 'viem' +import { readContract } from 'viem/actions' +import type { pdpVerifierAbi } from '../abis/generated.ts' +import { asChain } from '../chains.ts' +import type { ActionCallChain } from '../types.ts' + +export namespace getNextChallengeEpoch { + export type OptionsType = { + /** The ID of the data set to get next challenge epoch for. */ + dataSetId: bigint + /** PDP Verifier contract address. If not provided, the default is the PDP Verifier contract address for the chain. */ + contractAddress?: Address + } + + export type OutputType = bigint + /** + * `uint256` + */ + export type ContractOutputType = ContractFunctionReturnType< + typeof pdpVerifierAbi, + 'pure' | 'view', + 'getNextChallengeEpoch' + > + + export type ErrorType = asChain.ErrorType | ReadContractErrorType +} + +/** + * Get next challenge epoch + * + * @example + * ```ts + * import { getNextChallengeEpoch } from '@filoz/synapse-core/pdp-verifier' + * import { calibration } from '@filoz/synapse-core/chains' + * import { createPublicClient, http } from 'viem' + * + * const client = createPublicClient({ + * chain: calibration, + * transport: http(), + * }) + * + * const nextChallengeEpoch = await getNextChallengeEpoch(client, { + * dataSetId: 1n, + * }) + * ``` + * + * @param client - The client to use to get the active pieces. + * @param options - {@link getNextChallengeEpoch.OptionsType} + * @returns The next challenge epoch for the data set {@link getNextChallengeEpoch.OutputType} + * @throws Errors {@link getNextChallengeEpoch.ErrorType} + */ +export async function getNextChallengeEpoch( + client: Client, + options: getNextChallengeEpoch.OptionsType +): Promise { + const data = await readContract( + client, + getNextChallengeEpochCall({ + chain: client.chain, + dataSetId: options.dataSetId, + contractAddress: options.contractAddress, + }) + ) + return data +} + +export namespace getNextChallengeEpochCall { + export type OptionsType = Simplify + export type ErrorType = asChain.ErrorType + export type OutputType = ContractFunctionParameters +} + +/** + * Create a call to the {@link getNextChallengeEpoch} function for use with the multicall or readContract function. + * + * @example + * ```ts + * import { getNextChallengeEpochCall } from '@filoz/synapse-core/pdp-verifier' + * import { calibration } from '@filoz/synapse-core/chains' + * import { createPublicClient, http } from 'viem' + * import { multicall } from 'viem/actions' + * + * const client = createPublicClient({ + * chain: calibration, + * transport: http(), + * }) + * + * const results = await multicall(client, { + * contracts: [ + * getNextChallengeEpochCall({ chain: calibration, dataSetId: 1n }), + * getNextChallengeEpochCall({ chain: calibration, dataSetId: 101n }), + * ], + * }) + * ``` + * + * @param options - {@link getNextChallengeEpochCall.OptionsType} + * @returns The call to the getNextChallengeEpoch function {@link getNextChallengeEpochCall.OutputType} + * @throws Errors {@link getNextChallengeEpochCall.ErrorType} + */ +export function getNextChallengeEpochCall(options: getNextChallengeEpochCall.OptionsType) { + const chain = asChain(options.chain) + return { + abi: chain.contracts.pdp.abi, + address: options.contractAddress ?? chain.contracts.pdp.address, + functionName: 'getNextChallengeEpoch', + args: [options.dataSetId], + } satisfies getNextChallengeEpochCall.OutputType +} diff --git a/packages/synapse-core/src/pdp-verifier/index.ts b/packages/synapse-core/src/pdp-verifier/index.ts index bd68d23cb..537613512 100644 --- a/packages/synapse-core/src/pdp-verifier/index.ts +++ b/packages/synapse-core/src/pdp-verifier/index.ts @@ -16,6 +16,7 @@ export * from './get-data-set-leaf-count.ts' export * from './get-data-set-listener.ts' export * from './get-data-set-storage-provider.ts' export * from './get-dataset-size.ts' +export * from './get-next-challenge-epoch.ts' export * from './get-next-piece-id.ts' export * from './get-pieces.ts' export * from './get-scheduled-removals.ts' diff --git a/packages/synapse-sdk/src/storage/context.ts b/packages/synapse-sdk/src/storage/context.ts index 1b2093675..cec5548a5 100644 --- a/packages/synapse-sdk/src/storage/context.ts +++ b/packages/synapse-sdk/src/storage/context.ts @@ -1090,31 +1090,6 @@ export class StorageContext { return hash } - /** - * Check if a piece exists on this service provider. - * - * @param options - Options for the has piece operation - * @param options.pieceCid - The PieceCID (piece CID) to check - * @returns True if the piece exists on this provider, false otherwise - */ - async hasPiece(options: { pieceCid: string | PieceCID }): Promise { - const { pieceCid } = options - const parsedPieceCID = Piece.asPieceCID(pieceCid) - if (parsedPieceCID == null) { - return false - } - - try { - await SP.findPiece({ - serviceURL: this._pdpEndpoint, - pieceCid: parsedPieceCID, - }) - return true - } catch { - return false - } - } - /** * Check if a piece exists on this service provider and get its proof status. * Also returns timing information about when the piece was last proven and when the next @@ -1126,9 +1101,9 @@ export class StorageContext { * * @param options - Options for the piece status * @param options.pieceCid - The PieceCID (piece CID) to check - * @returns Status information including existence, data set timing, and retrieval URL + * @returns Status information including data set timing and retrieval URL */ - async pieceStatus(options: { pieceCid: string | PieceCID }): Promise { + async pieceStatus(options: { pieceCid: string | PieceCID }): Promise { if (this.dataSetId == null) { throw createError('StorageContext', 'pieceStatus', 'Data set not found') } @@ -1138,18 +1113,26 @@ export class StorageContext { } // Run multiple operations in parallel for better performance - const [exists, dataSetData, currentEpoch] = await Promise.all([ - // Check if piece exists on provider - this.hasPiece({ pieceCid: parsedPieceCID }), - // Get data set data - SP.getDataSet({ - serviceURL: this._pdpEndpoint, + const [activePieces, nextChallengeEpoch, currentEpoch, pdpConfig, providerInfo] = await Promise.all([ + PDPVerifier.getActivePieces(this._client, { + dataSetId: this.dataSetId, + }), + PDPVerifier.getNextChallengeEpoch(this._client, { dataSetId: this.dataSetId, }), - // Get current epoch getBlockNumber(this._client), + this._warmStorageService.getPDPConfig().catch((error) => { + console.debug('Failed to get PDP config:', error) + return null + }), + this.getProviderInfo().catch(() => null), ]) + const pieceData = activePieces.pieces.find((piece) => piece.cid.equals(parsedPieceCID)) + if (pieceData === undefined) { + return null + } + // Initialize return values let retrievalUrl: string | null = null let pieceId: bigint | undefined @@ -1159,80 +1142,60 @@ export class StorageContext { let hoursUntilChallengeWindow = 0 let isProofOverdue = false - // If piece exists, get provider info for retrieval URL and proving params in parallel - if (exists) { - const [providerInfo, pdpConfig] = await Promise.all([ - // Get provider info for retrieval URL - this.getProviderInfo().catch(() => null), - dataSetData != null - ? this._warmStorageService.getPDPConfig().catch((error) => { - console.debug('Failed to get PDP config:', error) - return null - }) - : Promise.resolve(null), - ]) - - // Set retrieval URL if we have provider info - if (providerInfo != null) { - retrievalUrl = createPieceUrlPDP({ - cid: parsedPieceCID.toString(), - serviceURL: providerInfo.pdp.serviceURL, - }) - } + // Set retrieval URL if we have provider info + if (providerInfo != null) { + retrievalUrl = createPieceUrlPDP({ + cid: parsedPieceCID.toString(), + serviceURL: providerInfo.pdp.serviceURL, + }) + } - // Process proof timing data if we have data set data and PDP config - if (dataSetData != null && pdpConfig != null) { - // Check if this PieceCID is in the data set - const pieceData = dataSetData.pieces.find((piece) => piece.pieceCid.toString() === parsedPieceCID.toString()) - - if (pieceData != null) { - pieceId = pieceData.pieceId - - // Calculate timing based on nextChallengeEpoch - if (dataSetData.nextChallengeEpoch > 0) { - // nextChallengeEpoch is when the challenge window STARTS, not ends! - // The proving deadline is nextChallengeEpoch + challengeWindowSize - const challengeWindowStart = dataSetData.nextChallengeEpoch - const provingDeadline = challengeWindowStart + Number(pdpConfig.challengeWindowSize) - - // Calculate when the next proof is due (end of challenge window) - nextProofDue = epochToDate(provingDeadline, this._chain.genesisTimestamp) - - // Calculate last proven date (one proving period before next challenge) - const lastProvenDate = calculateLastProofDate( - dataSetData.nextChallengeEpoch, - Number(pdpConfig.maxProvingPeriod), - this._chain.genesisTimestamp - ) - if (lastProvenDate != null) { - lastProven = lastProvenDate - } + // Process proof timing data if we have data set data and PDP config + if (pdpConfig != null) { + pieceId = pieceData.id + + // Calculate timing based on nextChallengeEpoch + if (nextChallengeEpoch > 0n) { + // nextChallengeEpoch is when the challenge window STARTS, not ends! + // The proving deadline is nextChallengeEpoch + challengeWindowSize + const challengeWindowStart = nextChallengeEpoch + const provingDeadline = challengeWindowStart + pdpConfig.challengeWindowSize + + // Calculate when the next proof is due (end of challenge window) + nextProofDue = epochToDate(Number(provingDeadline), this._chain.genesisTimestamp) + + // Calculate last proven date (one proving period before next challenge) + const lastProvenDate = calculateLastProofDate( + Number(nextChallengeEpoch), + Number(pdpConfig.maxProvingPeriod), + this._chain.genesisTimestamp + ) + if (lastProvenDate != null) { + lastProven = lastProvenDate + } - // Check if we're in the challenge window - inChallengeWindow = Number(currentEpoch) >= challengeWindowStart && Number(currentEpoch) < provingDeadline + // Check if we're in the challenge window + inChallengeWindow = Number(currentEpoch) >= challengeWindowStart && Number(currentEpoch) < provingDeadline - // Check if proof is overdue (past the proving deadline) - isProofOverdue = Number(currentEpoch) >= provingDeadline + // Check if proof is overdue (past the proving deadline) + isProofOverdue = Number(currentEpoch) >= provingDeadline - // Calculate hours until challenge window starts (only if before challenge window) - if (Number(currentEpoch) < challengeWindowStart) { - const timeUntil = timeUntilEpoch(challengeWindowStart, Number(currentEpoch)) - hoursUntilChallengeWindow = timeUntil.hours - } - } else { - // If nextChallengeEpoch is 0, it might mean: - // 1. Proof was just submitted and system is updating - // 2. Data set is not active - // In case 1, we might have just proven, so set lastProven to very recent - // This is a temporary state and should resolve quickly - console.debug('Data set has nextChallengeEpoch=0, may have just been proven') - } + // Calculate hours until challenge window starts (only if before challenge window) + if (Number(currentEpoch) < challengeWindowStart) { + const timeUntil = timeUntilEpoch(Number(challengeWindowStart), Number(currentEpoch)) + hoursUntilChallengeWindow = timeUntil.hours } + } else { + // If nextChallengeEpoch is 0, it might mean: + // 1. Proof was just submitted and system is updating + // 2. Data set is not active + // In case 1, we might have just proven, so set lastProven to very recent + // This is a temporary state and should resolve quickly + console.debug('Data set has nextChallengeEpoch=0, may have just been proven') } } return { - exists, dataSetLastProven: lastProven, dataSetNextProofDue: nextProofDue, retrievalUrl, diff --git a/packages/synapse-sdk/src/storage/manager.ts b/packages/synapse-sdk/src/storage/manager.ts index 9aae8e198..515b9964f 100644 --- a/packages/synapse-sdk/src/storage/manager.ts +++ b/packages/synapse-sdk/src/storage/manager.ts @@ -64,7 +64,7 @@ import type { UploadCosts, UploadResult, } from '../types.ts' -import { combineMetadata, createError, METADATA_KEYS, SIZE_CONSTANTS, TIME_CONSTANTS } from '../utils/index.ts' +import { combineMetadata, createError, SIZE_CONSTANTS, TIME_CONSTANTS } from '../utils/index.ts' import type { WarmStorageService } from '../warm-storage/index.ts' import { StorageContext } from './context.ts' diff --git a/packages/synapse-sdk/src/test/storage.test.ts b/packages/synapse-sdk/src/test/storage.test.ts index eea766036..7822735a2 100644 --- a/packages/synapse-sdk/src/test/storage.test.ts +++ b/packages/synapse-sdk/src/test/storage.test.ts @@ -1410,35 +1410,21 @@ describe('StorageService', () => { describe('pieceStatus()', () => { const mockPieceCID = 'bafkzcibeqcad6efnpwn62p5vvs5x3nh3j7xkzfgb3xtitcdm2hulmty3xx4tl3wace' - it('should return exists=false when piece not found on provider', async () => { + it('should return exists=false when piece not in data set', async () => { server.use( Mocks.JSONRPC({ ...Mocks.presets.basic, - }), - Mocks.PING(), - http.get('https://pdp.example.com/pdp/data-sets/:id', async () => { - return HttpResponse.json({ - id: 1, - pieces: [], - nextChallengeEpoch: 5000, - }) - }), - http.get('https://pdp.example.com/pdp/piece', async () => { - return HttpResponse.text('Piece not found or does not belong to service', { - status: 404, - }) }) ) const synapse = new Synapse({ client, source: null }) const warmStorageService = new WarmStorageService({ client }) const service = await StorageContext.create({ synapse, warmStorageService, dataSetId: 1n }) - const status = await service.pieceStatus({ pieceCid: mockPieceCID }) + const status = await service.pieceStatus({ + pieceCid: 'bafkzcibduukaynfuioybwrsevewtttso22ucohqntpc5h7crizsaw5h7gxd74eav', + }) - assert.isFalse(status.exists) - assert.isNull(status.retrievalUrl) - assert.isNull(status.dataSetLastProven) - assert.isNull(status.dataSetNextProofDue) + assert.isNull(status) }) it('should return piece status with proof timing when piece exists', async () => { @@ -1446,24 +1432,6 @@ describe('StorageService', () => { Mocks.JSONRPC({ ...Mocks.presets.basic, eth_blockNumber: numberToHex(4000n), - }), - Mocks.PING(), - http.get('https://pdp.example.com/pdp/data-sets/:id', async () => { - return HttpResponse.json({ - id: 1, - pieces: [ - { - pieceId: 1, - pieceCid: mockPieceCID, - subPieceCid: mockPieceCID, - subPieceOffset: 0, - }, - ], - nextChallengeEpoch: 5000, - }) - }), - http.get('https://pdp.example.com/pdp/piece', async () => { - return HttpResponse.json({ pieceCid: mockPieceCID }) }) ) const synapse = new Synapse({ client, source: null }) @@ -1472,7 +1440,7 @@ describe('StorageService', () => { const status = await service.pieceStatus({ pieceCid: mockPieceCID }) - assert.isTrue(status.exists) + assert.isNotNull(status) assert.equal(status.retrievalUrl, `https://pdp.example.com/piece/${mockPieceCID}`) assert.isNotNull(status.dataSetLastProven) assert.isNotNull(status.dataSetNextProofDue) @@ -1485,30 +1453,14 @@ describe('StorageService', () => { Mocks.JSONRPC({ ...Mocks.presets.basic, eth_blockNumber: numberToHex(5030n), - }), - Mocks.PING(), - http.get('https://pdp.example.com/pdp/data-sets/:id', async () => { - return HttpResponse.json({ - id: 1, - pieces: [ - { - pieceId: 1, - pieceCid: mockPieceCID, - subPieceCid: mockPieceCID, - subPieceOffset: 0, - }, - ], - nextChallengeEpoch: 5000, - }) - }), - Mocks.pdp.findPieceHandler(mockPieceCID, true, pdpOptions) + }) ) const synapse = new Synapse({ client, source: null }) const warmStorageService = new WarmStorageService({ client }) const service = await StorageContext.create({ synapse, warmStorageService, dataSetId: 1n }) const status = await service.pieceStatus({ pieceCid: mockPieceCID }) - assert.isTrue(status.exists) + assert.isNotNull(status) // During challenge window assert.isTrue(status.inChallengeWindow) assert.isFalse(status.isProofOverdue) @@ -1518,25 +1470,7 @@ describe('StorageService', () => { server.use( Mocks.JSONRPC({ ...Mocks.presets.basic, - eth_blockNumber: numberToHex(5100n), - }), - Mocks.PING(), - http.get('https://pdp.example.com/pdp/data-sets/:id', async () => { - return HttpResponse.json({ - id: 1, - pieces: [ - { - pieceId: 1, - pieceCid: mockPieceCID, - subPieceCid: mockPieceCID, - subPieceOffset: 0, - }, - ], - nextChallengeEpoch: 5000, - }) - }), - http.get('https://pdp.example.com/pdp/piece', async () => { - return HttpResponse.json({ pieceCid: mockPieceCID }) + eth_blockNumber: numberToHex(5080n), }) ) const synapse = new Synapse({ client, source: null }) @@ -1545,7 +1479,7 @@ describe('StorageService', () => { const status = await service.pieceStatus({ pieceCid: mockPieceCID }) - assert.isTrue(status.exists) + assert.isNotNull(status) assert.isTrue(status.isProofOverdue) }) @@ -1554,24 +1488,10 @@ describe('StorageService', () => { Mocks.JSONRPC({ ...Mocks.presets.basic, eth_blockNumber: numberToHex(5100n), - }), - Mocks.PING(), - http.get('https://pdp.example.com/pdp/data-sets/:id', async () => { - return HttpResponse.json({ - id: 1, - pieces: [ - { - pieceId: 1, - pieceCid: mockPieceCID, - subPieceCid: mockPieceCID, - subPieceOffset: 0, - }, - ], - nextChallengeEpoch: 0, - }) - }), - http.get('https://pdp.example.com/pdp/piece', async () => { - return HttpResponse.json({ pieceCid: mockPieceCID }) + pdpVerifier: { + ...Mocks.presets.basic.pdpVerifier, + getNextChallengeEpoch: () => [0n], + }, }) ) const synapse = new Synapse({ client, source: null }) @@ -1580,7 +1500,7 @@ describe('StorageService', () => { const status = await service.pieceStatus({ pieceCid: mockPieceCID }) - assert.isTrue(status.exists) + assert.isNotNull(status) assert.isNull(status.dataSetLastProven) // No challenge means no proof data assert.isNull(status.dataSetNextProofDue) assert.isFalse(status.inChallengeWindow) @@ -1591,24 +1511,6 @@ describe('StorageService', () => { Mocks.JSONRPC({ ...Mocks.presets.basic, eth_blockNumber: numberToHex(5100n), - }), - Mocks.PING(), - http.get('https://pdp.example.com/pdp/data-sets/:id', async () => { - return HttpResponse.json({ - id: 1, - pieces: [ - { - pieceId: 1, - pieceCid: mockPieceCID, - subPieceCid: mockPieceCID, - subPieceOffset: 0, - }, - ], - nextChallengeEpoch: 0, - }) - }), - http.get('https://pdp.example.com/pdp/piece', async () => { - return HttpResponse.json({ pieceCid: mockPieceCID }) }) ) const synapse = new Synapse({ client, source: null }) @@ -1617,7 +1519,7 @@ describe('StorageService', () => { const status = await service.pieceStatus({ pieceCid: mockPieceCID }) - assert.isTrue(status.exists) + assert.isNotNull(status) // Should not have double slash assert.equal(status.retrievalUrl, `https://pdp.example.com/piece/${mockPieceCID}`) // Check that the URL doesn't contain double slashes after the protocol @@ -1629,8 +1531,7 @@ describe('StorageService', () => { server.use( Mocks.JSONRPC({ ...Mocks.presets.basic, - }), - Mocks.PING() + }) ) const synapse = new Synapse({ client, source: null }) const warmStorageService = new WarmStorageService({ client }) @@ -1649,24 +1550,6 @@ describe('StorageService', () => { Mocks.JSONRPC({ ...Mocks.presets.basic, eth_blockNumber: numberToHex(4880n), - }), - Mocks.PING(), - http.get('https://pdp.example.com/pdp/data-sets/:id', async () => { - return HttpResponse.json({ - id: 1, - pieces: [ - { - pieceId: 1, - pieceCid: mockPieceCID, - subPieceCid: mockPieceCID, - subPieceOffset: 0, - }, - ], - nextChallengeEpoch: 5000, - }) - }), - http.get('https://pdp.example.com/pdp/piece', async () => { - return HttpResponse.json({ pieceCid: mockPieceCID }) }) ) const synapse = new Synapse({ client, source: null }) @@ -1675,7 +1558,7 @@ describe('StorageService', () => { const status = await service.pieceStatus({ pieceCid: mockPieceCID }) - assert.isTrue(status.exists) + assert.isNotNull(status) assert.isFalse(status.inChallengeWindow) // Not yet in challenge window assert.isTrue((status.hoursUntilChallengeWindow ?? 0) > 0) }) diff --git a/packages/synapse-sdk/src/types.ts b/packages/synapse-sdk/src/types.ts index 6e1ea3d77..9c08bd43f 100644 --- a/packages/synapse-sdk/src/types.ts +++ b/packages/synapse-sdk/src/types.ts @@ -657,8 +657,6 @@ export interface DataSetPieceData { * The timing information reflects the data set's status. */ export interface PieceStatus { - /** Whether the piece exists on the service provider */ - exists: boolean /** When the data set containing this piece was last proven on-chain (null if never proven or not yet due) */ dataSetLastProven: Date | null /** When the next proof is due for the data set containing this piece (end of challenge window) */ diff --git a/utils/example-piece-status.js b/utils/example-piece-status.js index e9d69ae18..5edc73805 100755 --- a/utils/example-piece-status.js +++ b/utils/example-piece-status.js @@ -285,7 +285,7 @@ async function runPieceMode(synapse, pieceCid, options) { console.log('Checking piece status...\n') const status = await storageContext.pieceStatus({ pieceCid }) - if (!status.exists) { + if (status === null) { console.log('Piece does not exist on the selected service provider') return }