Skip to content

Commit 109dbd8

Browse files
Kubuxurvagg
authored andcommitted
feat: create low-level create-and-add API (#347)
* feat: create low-level create-and-add API Signed-off-by: Jakub Sztandera <[email protected]> * add doc strings Signed-off-by: Jakub Sztandera <[email protected]> --------- Signed-off-by: Jakub Sztandera <[email protected]>
1 parent f6d8128 commit 109dbd8

File tree

2 files changed

+184
-42
lines changed

2 files changed

+184
-42
lines changed

packages/synapse-sdk/src/pdp/server.ts

Lines changed: 155 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -146,12 +146,20 @@ export interface PieceAdditionStatusResponse {
146146
* Input for adding pieces to a data set
147147
*/
148148
export interface PDPAddPiecesInput {
149-
pieces: {
150-
pieceCid: string
151-
subPieces: {
152-
subPieceCid: string
153-
}[]
149+
pieces: PDPPieces[]
150+
extraData: string
151+
}
152+
153+
export interface PDPPieces {
154+
pieceCid: string
155+
subPieces: {
156+
subPieceCid: string
154157
}[]
158+
}
159+
160+
export interface PDPCreateAndAddInput {
161+
recordKeeper: string
162+
pieces: PDPPieces[]
155163
extraData: string
156164
}
157165

@@ -177,6 +185,7 @@ export class PDPServer {
177185
* Create a new data set on the PDP server
178186
* @param clientDataSetId - Unique ID for the client's dataset
179187
* @param payee - Address that will receive payments (service provider)
188+
* @param payer - Address that will pay for the storage (client)
180189
* @param metadata - Metadata entries for the data set (key-value pairs)
181190
* @param recordKeeper - Address of the Warm Storage contract
182191
* @returns Promise that resolves with transaction hash and status URL
@@ -245,36 +254,114 @@ export class PDPServer {
245254
}
246255

247256
/**
248-
* Add pieces to an existing data set
249-
* @param dataSetId - The ID of the data set to add pieces to
250-
* @param clientDataSetId - The client's dataset ID used when creating the data set
251-
* @param nextPieceId - The ID to assign to the first piece being added, this should be
252-
* the next available ID on chain or the signature will fail to be validated
257+
* Creates a data set and adds pieces to it in a combined operation.
258+
* Users can poll the status of the operation using the returned data set status URL.
259+
* After which the user can use the returned transaction hash and data set ID to check the status of the piece addition.
260+
* @param clientDataSetId - Unique ID for the client's dataset
261+
* @param payee - Address that will receive payments (service provider)
262+
* @param payer - Address that will pay for the storage (client)
263+
* @param recordKeeper - Address of the Warm Storage contract
253264
* @param pieceDataArray - Array of piece data containing PieceCID CIDs and raw sizes
254-
* @param metadata - Optional metadata for each piece (array of arrays, one per piece)
255-
* @returns Promise that resolves when the pieces are added (201 Created)
256-
* @throws Error if any CID is invalid
257-
*
258-
* @example
259-
* ```typescript
260-
* const pieceData = ['bafkzcibcd...']
261-
* const metadata = [[{ key: 'snapshotDate', value: '20250711' }]]
262-
* await pdpTool.addPieces(dataSetId, clientDataSetId, nextPieceId, pieceData, metadata)
263-
* ```
265+
* @param metadata - Optional metadata for dataset and each of the pieces.
266+
* @returns Promise that resolves with transaction hash and status URL
264267
*/
265-
async addPieces(
266-
dataSetId: number,
268+
async createAndAddPieces(
267269
clientDataSetId: bigint,
268-
nextPieceId: number,
270+
payee: string,
271+
payer: string,
272+
recordKeeper: string,
273+
pieceDataArray: PieceCID[] | string[],
274+
metadata: {
275+
dataset?: MetadataEntry[]
276+
pieces?: MetadataEntry[][]
277+
}
278+
): Promise<CreateDataSetResponse> {
279+
// Validate metadata against contract limits
280+
if (metadata.dataset == null) {
281+
metadata.dataset = []
282+
}
283+
validateDataSetMetadata(metadata.dataset)
284+
metadata.pieces = PDPServer._processAddPiecesInputs(pieceDataArray, metadata.pieces)
285+
286+
// Generate the EIP-712 signature for data set creation
287+
const createAuthData = await this.getAuthHelper().signCreateDataSet(clientDataSetId, payee, metadata.dataset)
288+
289+
// Prepare the extra data for the contract call
290+
// This needs to match the DataSetCreateData struct in Warm Storage contract
291+
const createExtraData = this._encodeDataSetCreateData({
292+
payer,
293+
clientDataSetId,
294+
metadata: metadata.dataset,
295+
signature: createAuthData.signature,
296+
})
297+
298+
const addAuthData = await this.getAuthHelper().signAddPieces(
299+
clientDataSetId,
300+
BigInt(0),
301+
pieceDataArray, // Pass PieceData[] directly to auth helper
302+
metadata.pieces
303+
)
304+
305+
const addExtraData = this._encodeAddPiecesExtraData({
306+
signature: addAuthData.signature,
307+
metadata: metadata.pieces,
308+
})
309+
310+
const abiCoder = ethers.AbiCoder.defaultAbiCoder()
311+
const encoded = abiCoder.encode(['bytes', 'bytes'], [`0x${createExtraData}`, `0x${addExtraData}`])
312+
const requestJson: PDPCreateAndAddInput = {
313+
recordKeeper: recordKeeper,
314+
pieces: PDPServer._formatPieceDataArrayForCurio(pieceDataArray),
315+
extraData: `${encoded}`,
316+
}
317+
318+
// Make the POST request to add pieces to the data set
319+
const response = await fetch(`${this._serviceURL}/pdp/data-sets/create-and-add`, {
320+
method: 'POST',
321+
headers: {
322+
'Content-Type': 'application/json',
323+
},
324+
body: JSON.stringify(requestJson),
325+
})
326+
327+
if (response.status !== 201) {
328+
const errorText = await response.text()
329+
throw new Error(`Failed to create data set: ${response.status} ${response.statusText} - ${errorText}`)
330+
}
331+
332+
// Extract transaction hash from Location header
333+
const location = response.headers.get('Location')
334+
if (location == null) {
335+
throw new Error('Server did not provide Location header in response')
336+
}
337+
338+
// Parse the location to extract the transaction hash
339+
// Expected format: /pdp/data-sets/created/{txHash}
340+
const locationMatch = location.match(/\/pdp\/data-sets\/created\/(.+)$/)
341+
if (locationMatch == null) {
342+
throw new Error(`Invalid Location header format: ${location}`)
343+
}
344+
345+
const txHash = locationMatch[1]
346+
347+
return {
348+
txHash,
349+
statusUrl: `${this._serviceURL}${location}`,
350+
}
351+
}
352+
353+
private static _processAddPiecesInputs(
269354
pieceDataArray: PieceCID[] | string[],
270355
metadata?: MetadataEntry[][]
271-
): Promise<AddPiecesResponse> {
356+
): MetadataEntry[][] {
272357
if (pieceDataArray.length === 0) {
273358
throw new Error('At least one piece must be provided')
274359
}
275360

276-
// Validate piece metadata against contract limits
277361
if (metadata != null) {
362+
if (metadata.length !== pieceDataArray.length) {
363+
throw new Error(`Metadata length (${metadata.length}) must match pieces length (${pieceDataArray.length})`)
364+
}
278365
for (let i = 0; i < metadata.length; i++) {
279366
if (metadata[i] != null && metadata[i].length > 0) {
280367
try {
@@ -293,15 +380,52 @@ export class PDPServer {
293380
throw new Error(`Invalid PieceCID: ${String(pieceData)}`)
294381
}
295382
}
296-
297383
// If no metadata provided, create empty arrays for each piece
298384
const finalMetadata = metadata ?? pieceDataArray.map(() => [])
385+
return finalMetadata
386+
}
299387

300-
// Validate metadata length matches pieces
301-
if (finalMetadata.length !== pieceDataArray.length) {
302-
throw new Error(`Metadata length (${finalMetadata.length}) must match pieces length (${pieceDataArray.length})`)
303-
}
388+
private static _formatPieceDataArrayForCurio(pieceDataArray: PieceCID[] | string[]): PDPPieces[] {
389+
return pieceDataArray.map((pieceData) => {
390+
// Convert to string for JSON serialization
391+
const cidString = typeof pieceData === 'string' ? pieceData : pieceData.toString()
392+
return {
393+
pieceCid: cidString,
394+
subPieces: [
395+
{
396+
subPieceCid: cidString, // Piece is its own subpiece
397+
},
398+
],
399+
}
400+
})
401+
}
304402

403+
/**
404+
* Add pieces to an existing data set
405+
* @param dataSetId - The ID of the data set to add pieces to
406+
* @param clientDataSetId - The client's dataset ID used when creating the data set
407+
* @param nextPieceId - The ID to assign to the first piece being added, this should be
408+
* the next available ID on chain or the signature will fail to be validated
409+
* @param pieceDataArray - Array of piece data containing PieceCID CIDs and raw sizes
410+
* @param metadata - Optional metadata for each piece (array of arrays, one per piece)
411+
* @returns Promise that resolves when the pieces are added (201 Created)
412+
* @throws Error if any CID is invalid
413+
*
414+
* @example
415+
* ```typescript
416+
* const pieceData = ['bafkzcibcd...']
417+
* const metadata = [[{ key: 'snapshotDate', value: '20250711' }]]
418+
* await pdpTool.addPieces(dataSetId, clientDataSetId, nextPieceId, pieceData, metadata)
419+
* ```
420+
*/
421+
async addPieces(
422+
dataSetId: number,
423+
clientDataSetId: bigint,
424+
nextPieceId: number,
425+
pieceDataArray: PieceCID[] | string[],
426+
metadata?: MetadataEntry[][]
427+
): Promise<AddPiecesResponse> {
428+
const finalMetadata = PDPServer._processAddPiecesInputs(pieceDataArray, metadata)
305429
// Generate the EIP-712 signature for adding pieces
306430
const authData = await this.getAuthHelper().signAddPieces(
307431
clientDataSetId,
@@ -320,18 +444,7 @@ export class PDPServer {
320444
// Prepare request body matching the Curio handler expectation
321445
// Each piece has itself as its only subPiece (internal implementation detail)
322446
const requestBody: PDPAddPiecesInput = {
323-
pieces: pieceDataArray.map((pieceData) => {
324-
// Convert to string for JSON serialization
325-
const cidString = typeof pieceData === 'string' ? pieceData : pieceData.toString()
326-
return {
327-
pieceCid: cidString,
328-
subPieces: [
329-
{
330-
subPieceCid: cidString, // Piece is its own subpiece
331-
},
332-
],
333-
}
334-
}),
447+
pieces: PDPServer._formatPieceDataArrayForCurio(pieceDataArray),
335448
extraData: `0x${extraData}`,
336449
}
337450

packages/synapse-sdk/src/test/pdp-server.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,35 @@ describe('PDPServer', () => {
9595
})
9696
})
9797

98+
describe('createAndAddPieces', () => {
99+
it('should handle successful data set creation', async () => {
100+
// Mock the createDataSet endpoint
101+
const mockTxHash = '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef'
102+
const validPieceCid = ['bafkzcibcd4bdomn3tgwgrh3g532zopskstnbrd2n3sxfqbze7rxt7vqn7veigmy']
103+
104+
server.use(
105+
http.post('http://pdp.local/pdp/data-sets/create-and-add', () => {
106+
return new HttpResponse(null, {
107+
status: 201,
108+
headers: { Location: `/pdp/data-sets/created/${mockTxHash}` },
109+
})
110+
})
111+
)
112+
113+
const result = await pdpServer.createAndAddPieces(
114+
0n,
115+
'0x70997970C51812dc3A010C7d01b50e0d17dc79C8',
116+
await signer.getAddress(),
117+
TEST_CONTRACT_ADDRESS,
118+
validPieceCid,
119+
{}
120+
)
121+
122+
assert.strictEqual(result.txHash, mockTxHash)
123+
assert.include(result.statusUrl, mockTxHash)
124+
})
125+
})
126+
98127
describe('getPieceAdditionStatus', () => {
99128
it('should handle successful status check', async () => {
100129
const mockTxHash = '0x7890abcdef1234567890abcdef1234567890abcdef1234567890abcdef123456'

0 commit comments

Comments
 (0)