diff --git a/packages/synapse-core/src/sp.ts b/packages/synapse-core/src/sp.ts index 6d708677..00a8e8f4 100644 --- a/packages/synapse-core/src/sp.ts +++ b/packages/synapse-core/src/sp.ts @@ -841,6 +841,48 @@ export async function deletePiece(options: deletePiece.OptionsType): Promise { + const { endpoint, dataSetId, pieceIds, extraData } = options + const response = await request.json.post( + new URL(`pdp/data-sets/${dataSetId}/pieces/removals`, endpoint), + { + body: { pieceIds: pieceIds.map((id) => id.toString()), extraData }, + timeout: TIMEOUT, + } + ) + + if (response.error) { + if (HttpError.is(response.error)) { + throw new DeletePieceError(await response.error.response.text()) + } + throw response.error + } + + return response.result +} + /** * Ping the PDP API. * diff --git a/packages/synapse-core/src/warm-storage/pieces.ts b/packages/synapse-core/src/warm-storage/pieces.ts index 9f8f9f5f..e45fe0d8 100644 --- a/packages/synapse-core/src/warm-storage/pieces.ts +++ b/packages/synapse-core/src/warm-storage/pieces.ts @@ -133,6 +133,40 @@ export async function waitForDeletePieceStatus( return receipt } +export type DeletePiecesOptions = { + pieceIds: bigint[] + dataSetId: bigint + clientDataSetId: bigint + endpoint: string +} + +/** + * Delete multiple pieces from a data set + * + * Call the Service Provider API to delete the pieces. + * + * @param client - The client to use to delete the pieces. + * @param options - The options for the delete pieces. + * @param options.dataSetId - The ID of the data set. + * @param options.clientDataSetId - The ID of the client data set. + * @param options.pieceIds - The IDs of the pieces. + * @param options.endpoint - The endpoint of the PDP API. + * @returns The transaction hashes of the delete operations. + */ +export async function deletePieces(client: Client, options: DeletePiecesOptions) { + const extraData = await signSchedulePieceRemovals(client, { + clientDataSetId: options.clientDataSetId, + pieceIds: options.pieceIds, + }) + + return PDP.deletePieces({ + endpoint: options.endpoint, + dataSetId: options.dataSetId, + pieceIds: options.pieceIds, + extraData, + }) +} + export type GetPiecesOptions = { dataSet: DataSet address: Address diff --git a/packages/synapse-core/test/sp.test.ts b/packages/synapse-core/test/sp.test.ts index af8dcf7c..fad6f66d 100644 --- a/packages/synapse-core/test/sp.test.ts +++ b/packages/synapse-core/test/sp.test.ts @@ -836,6 +836,41 @@ InvalidSignature(address expected, address actual) }) }) + describe('deletePieces', () => { + it('should handle successful batch delete', async () => { + const mockTxHash = '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef' + const mockResponse = { + txHash: mockTxHash, + } + + server.use( + http.post('http://pdp.local/pdp/data-sets/1/pieces/removals', async ({ request }) => { + const body = (await request.json()) as { pieceIds: string[]; extraData: string } + assert.hasAllKeys(body, ['pieceIds', 'extraData']) + assert.deepStrictEqual(body.pieceIds, ['2', '3']) + assert.isDefined(body.extraData) + return HttpResponse.json(mockResponse, { + status: 200, + }) + }) + ) + + const extraData = await TypedData.signSchedulePieceRemovals(client, { + clientDataSetId: 0n, + pieceIds: [2n, 3n], + }) + + const result = await SP.deletePieces({ + endpoint: 'http://pdp.local', + dataSetId: 1n, + pieceIds: [2n, 3n], + extraData, + }) + + assert.strictEqual(result.txHash, mockTxHash) + }) + }) + describe('findPiece', () => { const mockPieceCidStr = 'bafkzcibcd4bdomn3tgwgrh3g532zopskstnbrd2n3sxfqbze7rxt7vqn7veigmy' diff --git a/packages/synapse-react/src/warm-storage/index.ts b/packages/synapse-react/src/warm-storage/index.ts index afe8eab1..88731da5 100644 --- a/packages/synapse-react/src/warm-storage/index.ts +++ b/packages/synapse-react/src/warm-storage/index.ts @@ -1,6 +1,7 @@ export * from './use-create-data-set.ts' export * from './use-data-sets.ts' export * from './use-delete-piece.ts' +export * from './use-delete-pieces.ts' export * from './use-providers.ts' export * from './use-service-price.ts' export * from './use-upload.ts' diff --git a/packages/synapse-react/src/warm-storage/use-delete-pieces.ts b/packages/synapse-react/src/warm-storage/use-delete-pieces.ts new file mode 100644 index 00000000..b740bd40 --- /dev/null +++ b/packages/synapse-react/src/warm-storage/use-delete-pieces.ts @@ -0,0 +1,65 @@ +import { getChain } from '@filoz/synapse-core/chains' +import type { SessionKey } from '@filoz/synapse-core/session-key' +import { type DataSet, deletePieces, waitForDeletePieceStatus } from '@filoz/synapse-core/warm-storage' +import { type MutateOptions, useMutation, useQueryClient } from '@tanstack/react-query' +import type { TransactionReceipt } from 'viem' +import { useAccount, useChainId, useConfig } from 'wagmi' +import { getConnectorClient } from 'wagmi/actions' + +export interface UseDeletePiecesProps { + /** + * The callback to call when the hash is available. + */ + onHash?: (hash: string) => void + sessionKey?: SessionKey + mutation?: Omit, 'mutationFn'> +} + +export interface UseDeletePiecesVariables { + dataSet: DataSet + pieceIds: bigint[] +} + +/** + * Hook to delete multiple pieces from a data set. + * + * @param props - {@link UseDeletePiecesProps} + * @returns + */ +export function useDeletePieces(props: UseDeletePiecesProps) { + const config = useConfig() + const chainId = useChainId({ config }) + const chain = getChain(chainId) + const account = useAccount({ config }) + const queryClient = useQueryClient() + const client = config.getClient() + + return useMutation({ + ...props?.mutation, + mutationFn: async ({ dataSet, pieceIds }: UseDeletePiecesVariables) => { + let connectorClient = await getConnectorClient(config, { + account: account.address, + chainId, + }) + + if (props?.sessionKey && (await props?.sessionKey.isValid(connectorClient, 'SchedulePieceRemovals'))) { + connectorClient = props?.sessionKey.client(chain, client.transport) + } + + const deletePiecesRsp = await deletePieces(connectorClient, { + endpoint: dataSet.pdp.serviceURL, + dataSetId: dataSet.dataSetId, + clientDataSetId: dataSet.clientDataSetId, + pieceIds, + }) + + props?.onHash?.(deletePiecesRsp.txHash) + const rsp = await waitForDeletePieceStatus(client, deletePiecesRsp) + + queryClient.invalidateQueries({ + queryKey: ['synapse-warm-storage-data-sets', account.address], + }) + return rsp + }, + }) +} diff --git a/packages/synapse-sdk/src/pdp/index.ts b/packages/synapse-sdk/src/pdp/index.ts index de448e74..9a1841e0 100644 --- a/packages/synapse-sdk/src/pdp/index.ts +++ b/packages/synapse-sdk/src/pdp/index.ts @@ -11,6 +11,7 @@ export type { AddPiecesResponse, CreateDataSetResponse, + DeletePiecesResponse, UploadPieceOptions, } from './server.ts' export { PDPServer } from './server.ts' diff --git a/packages/synapse-sdk/src/pdp/server.ts b/packages/synapse-sdk/src/pdp/server.ts index b160fe4f..4bcac565 100644 --- a/packages/synapse-sdk/src/pdp/server.ts +++ b/packages/synapse-sdk/src/pdp/server.ts @@ -33,6 +33,7 @@ import { addPieces, createDataSet, createDataSetAndAddPieces, + deletePieces, type PieceInputWithMetadata, } from '@filoz/synapse-core/warm-storage' import type { Account, Address, Chain, Client, Transport } from 'viem' @@ -60,6 +61,16 @@ export interface AddPiecesResponse { statusUrl: string } +/** + * Response from deleting pieces from a data set + */ +export interface DeletePiecesResponse { + /** Success message from the server */ + message: string + /** Transaction hash for the piece removal */ + txHash: string +} + /** * Options for uploading a piece */ @@ -251,4 +262,24 @@ export class PDPServer { nextChallengeEpoch: data.nextChallengeEpoch, } } + + /** + * Delete multiple pieces from a data set + * @param dataSetId - The ID of the data set + * @param clientDataSetId - The client's dataset ID used when creating the data set + * @param pieceIds - Array of piece IDs to delete + * @returns Promise that resolves with transaction hash + */ + async deletePieces(dataSetId: bigint, clientDataSetId: bigint, pieceIds: bigint[]): Promise { + const { txHash } = await deletePieces(this._client, { + pieceIds, + dataSetId, + clientDataSetId, + endpoint: this._endpoint, + }) + return { + message: `${pieceIds.length} pieces removed from data set ID ${dataSetId} successfully`, + txHash, + } + } }