Skip to content

Commit ac3552a

Browse files
committed
temp: add utils/example-sp-fetch-e2e.js fetch testing script
1 parent 7af205b commit ac3552a

1 file changed

Lines changed: 372 additions & 0 deletions

File tree

utils/example-sp-fetch-e2e.js

Lines changed: 372 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,372 @@
1+
#!/usr/bin/env node
2+
3+
/**
4+
* Example: SP-to-SP Piece Fetch End-to-End Test
5+
*
6+
* This example demonstrates the SP-to-SP fetch functionality:
7+
* 1. Upload a piece to SP1 (providerId=1) using low-level park API (no AddPieces)
8+
* 2. Wait for SP1 to park the piece
9+
* 3. Request SP2 (providerId=2) to fetch the piece from SP1
10+
* 4. Poll until the fetch completes
11+
* 5. Verify SP2 can serve the piece
12+
*
13+
* This tests:
14+
* - curio: POST /pdp/piece/fetch endpoint
15+
* - synapse-sdk: sp-fetch module
16+
*
17+
* Required environment variables:
18+
* - PRIVATE_KEY: Your private key (with 0x prefix)
19+
* - RPC_URL: Filecoin RPC endpoint (defaults to calibration)
20+
*
21+
* Optional environment variables (for devnet):
22+
* - WARM_STORAGE_ADDRESS: Warm Storage service contract address
23+
* - MULTICALL3_ADDRESS: Multicall3 address (required for devnet)
24+
* - USDFC_ADDRESS: USDFC token address
25+
*
26+
* Usage:
27+
* PRIVATE_KEY=0x... node example-sp-fetch-e2e.js <file-path>
28+
*
29+
* With foc-devnet:
30+
* RUN_ID=$(jq -r '.run_id' ~/.foc-devnet/state/current_runid.json)
31+
* PRIVATE_KEY=0x$(jq -r '.[] | select(.name=="USER_1") | .private_key' ~/.foc-devnet/keys/addresses.json) \
32+
* RPC_URL=http://localhost:$(docker port foc-${RUN_ID}-lotus 1234 | cut -d: -f2)/rpc/v1 \
33+
* WARM_STORAGE_ADDRESS=$(jq -r '.foc_contracts.filecoin_warm_storage_service_proxy' ~/.foc-devnet/state/latest/contract_addresses.json) \
34+
* MULTICALL3_ADDRESS=$(jq -r '.contracts.multicall' ~/.foc-devnet/state/latest/contract_addresses.json) \
35+
* USDFC_ADDRESS=$(jq -r '.contracts.usdfc' ~/.foc-devnet/state/latest/contract_addresses.json) \
36+
* SP_REGISTRY_ADDRESS=$(jq -r '.foc_contracts.service_provider_registry_proxy' ~/.foc-devnet/state/latest/contract_addresses.json) \
37+
* node utils/example-sp-fetch-e2e.js test-file.txt
38+
*/
39+
40+
import { ethers } from 'ethers'
41+
import fsPromises from 'fs/promises'
42+
import * as SP from '../packages/synapse-core/src/sp.ts'
43+
import * as spFetch from '../packages/synapse-core/src/sp-fetch.ts'
44+
import { randU256 } from '../packages/synapse-core/src/utils/rand.ts'
45+
import { Synapse } from '../packages/synapse-sdk/src/index.ts'
46+
import { PDPAuthHelper } from '../packages/synapse-sdk/src/pdp/auth.ts'
47+
import { SPRegistryService } from '../packages/synapse-sdk/src/sp-registry/service.ts'
48+
49+
// Configuration from environment
50+
const PRIVATE_KEY = process.env.PRIVATE_KEY
51+
const RPC_URL = process.env.RPC_URL || 'https://api.calibration.node.glif.io/rpc/v1'
52+
const WARM_STORAGE_ADDRESS = process.env.WARM_STORAGE_ADDRESS
53+
const MULTICALL3_ADDRESS = process.env.MULTICALL3_ADDRESS
54+
const USDFC_ADDRESS = process.env.USDFC_ADDRESS
55+
const SP_REGISTRY_ADDRESS = process.env.SP_REGISTRY_ADDRESS
56+
57+
function printUsageAndExit() {
58+
console.error('Usage: PRIVATE_KEY=0x... node example-sp-fetch-e2e.js <file-path>')
59+
process.exit(1)
60+
}
61+
62+
// Validate inputs
63+
if (!PRIVATE_KEY) {
64+
console.error('ERROR: PRIVATE_KEY environment variable is required')
65+
printUsageAndExit()
66+
}
67+
68+
const filePaths = process.argv.slice(2)
69+
if (filePaths.length === 0) {
70+
console.error('ERROR: At least one file path argument is required')
71+
printUsageAndExit()
72+
}
73+
74+
// Helper to format bytes for display
75+
function formatBytes(bytes) {
76+
if (bytes === 0) return '0 Bytes'
77+
const k = 1024
78+
const sizes = ['Bytes', 'KB', 'MB', 'GB']
79+
const i = Math.floor(Math.log(bytes) / Math.log(k))
80+
return `${parseFloat((bytes / k ** i).toFixed(2))} ${sizes[i]}`
81+
}
82+
83+
// Helper to format USDFC amounts (18 decimals)
84+
function formatUSDFC(amount) {
85+
const usdfc = Number(amount) / 1e18
86+
return `${usdfc.toFixed(6)} USDFC`
87+
}
88+
89+
/**
90+
* Encode CreateDataSet extra data for the fetch API (when dataSetId=0)
91+
* Format: (address payer, uint256 clientDataSetId, string[] keys, string[] values, bytes signature)
92+
*/
93+
function encodeCreateDataSetExtraData(payer, clientDataSetId, metadata, signature) {
94+
const sig = signature.startsWith('0x') ? signature : `0x${signature}`
95+
const keys = metadata.map((entry) => entry.key)
96+
const values = metadata.map((entry) => entry.value)
97+
98+
const abiCoder = ethers.AbiCoder.defaultAbiCoder()
99+
return abiCoder.encode(
100+
['address', 'uint256', 'string[]', 'string[]', 'bytes'],
101+
[payer, clientDataSetId, keys, values, sig]
102+
)
103+
}
104+
105+
/**
106+
* Encode AddPieces extra data for the fetch API
107+
* Format: (uint256 nonce, string[][] metadataKeys, string[][] metadataValues, bytes signature)
108+
*/
109+
function encodeAddPiecesExtraData(nonce, metadata, signature) {
110+
const sig = signature.startsWith('0x') ? signature : `0x${signature}`
111+
const keys = metadata.map((item) => item.map((entry) => entry.key))
112+
const values = metadata.map((item) => item.map((entry) => entry.value))
113+
114+
const abiCoder = ethers.AbiCoder.defaultAbiCoder()
115+
return abiCoder.encode(['uint256', 'string[][]', 'string[][]', 'bytes'], [nonce, keys, values, sig])
116+
}
117+
118+
/**
119+
* Encode combined extraData for creating a new data set with pieces (dataSetId=0)
120+
* Format: abi.encode(bytes createPayload, bytes addPayload)
121+
*/
122+
function encodeCombinedExtraData(createExtraData, addExtraData) {
123+
const abiCoder = ethers.AbiCoder.defaultAbiCoder()
124+
return abiCoder.encode(['bytes', 'bytes'], [createExtraData, addExtraData])
125+
}
126+
127+
async function main() {
128+
try {
129+
console.log('=== SP-to-SP Fetch E2E Test ===\n')
130+
console.log(`Processing ${filePaths.length} file(s)...`)
131+
132+
// Read all files and get their stats
133+
const fileInfos = await Promise.all(
134+
filePaths.map(async (filePath) => {
135+
const stat = await fsPromises.stat(filePath)
136+
if (!stat.isFile()) {
137+
throw new Error(`Path is not a file: ${filePath}`)
138+
}
139+
console.log(` ${filePath}: ${formatBytes(stat.size)}`)
140+
return { filePath, size: stat.size }
141+
})
142+
)
143+
144+
// Create Synapse instance
145+
console.log('\n--- Initializing Synapse SDK ---')
146+
console.log(`RPC URL: ${RPC_URL}`)
147+
148+
const synapseOptions = {
149+
multicall3Address: MULTICALL3_ADDRESS,
150+
privateKey: PRIVATE_KEY,
151+
rpcURL: RPC_URL,
152+
usdfcAddress: USDFC_ADDRESS,
153+
warmStorageAddress: WARM_STORAGE_ADDRESS,
154+
}
155+
156+
if (WARM_STORAGE_ADDRESS) {
157+
console.log(`Warm Storage Address: ${WARM_STORAGE_ADDRESS}`)
158+
}
159+
if (MULTICALL3_ADDRESS) {
160+
console.log(`Multicall3 Address: ${MULTICALL3_ADDRESS}`)
161+
}
162+
163+
const synapse = await Synapse.create(synapseOptions)
164+
console.log('Synapse instance created')
165+
166+
// Get wallet info
167+
const signer = synapse.getSigner()
168+
const address = await signer.getAddress()
169+
console.log(`Wallet address: ${address}`)
170+
171+
// Check balances
172+
console.log('\n--- Checking Balances ---')
173+
const filBalance = await synapse.payments.walletBalance()
174+
const usdfcBalance = await synapse.payments.walletBalance('USDFC')
175+
console.log(`FIL balance: ${Number(filBalance) / 1e18} FIL`)
176+
console.log(`USDFC balance: ${formatUSDFC(usdfcBalance)}`)
177+
178+
// Get SP1 and SP2 info
179+
console.log('\n--- Discovering Service Providers ---')
180+
console.log(`SP Registry Address: ${SP_REGISTRY_ADDRESS}`)
181+
const spRegistry = new SPRegistryService(synapse.getProvider(), SP_REGISTRY_ADDRESS, MULTICALL3_ADDRESS)
182+
const sp1Info = await spRegistry.getProvider(1)
183+
const sp2Info = await spRegistry.getProvider(2)
184+
185+
if (!sp1Info || !sp1Info.products.PDP?.data.serviceURL) {
186+
throw new Error('SP1 (providerId=1) not found or missing PDP service URL')
187+
}
188+
if (!sp2Info || !sp2Info.products.PDP?.data.serviceURL) {
189+
throw new Error('SP2 (providerId=2) not found or missing PDP service URL')
190+
}
191+
192+
const sp1Url = sp1Info.products.PDP.data.serviceURL.replace(/\/$/, '')
193+
const sp2Url = sp2Info.products.PDP.data.serviceURL.replace(/\/$/, '')
194+
195+
console.log(`SP1 (providerId=1): ${sp1Info.name}`)
196+
console.log(` Address: ${sp1Info.serviceProvider}`)
197+
console.log(` PDP URL: ${sp1Url}`)
198+
console.log(`SP2 (providerId=2): ${sp2Info.name}`)
199+
console.log(` Address: ${sp2Info.serviceProvider}`)
200+
console.log(` PDP URL: ${sp2Url}`)
201+
202+
// Upload all pieces to SP1 in parallel
203+
console.log('\n--- Uploading Pieces to SP1 (Park Only) ---')
204+
const uploadResults = await Promise.all(
205+
fileInfos.map(async ({ filePath, size }) => {
206+
const fileHandle = await fsPromises.open(filePath, 'r')
207+
const fileData = fileHandle.readableWebStream()
208+
209+
console.log(` Uploading ${filePath}...`)
210+
const result = await SP.uploadPieceStreaming({
211+
endpoint: sp1Url,
212+
data: fileData,
213+
size: size,
214+
})
215+
await fileHandle.close()
216+
217+
const pieceCid = result.pieceCid.toString()
218+
console.log(` ${filePath} -> ${pieceCid.slice(0, 30)}... (${formatBytes(result.size)})`)
219+
return { filePath, pieceCid, size: result.size }
220+
})
221+
)
222+
223+
console.log(`\nUploaded ${uploadResults.length} piece(s) to SP1`)
224+
225+
// Wait for all pieces to be parked on SP1
226+
console.log('\n--- Waiting for SP1 to park all pieces ---')
227+
await Promise.all(
228+
uploadResults.map(async ({ pieceCid }) => {
229+
await SP.findPiece({
230+
endpoint: sp1Url,
231+
pieceCid: pieceCid,
232+
})
233+
console.log(` Parked: ${pieceCid.slice(0, 30)}...`)
234+
})
235+
)
236+
console.log('All pieces parked on SP1')
237+
238+
// Get FWSS address for recordKeeper
239+
const fwssAddress = synapse.getWarmStorageAddress()
240+
console.log(`\nFWSS Address (recordKeeper): ${fwssAddress}`)
241+
242+
// Prepare extraData for fetch request
243+
console.log('\n--- Preparing Fetch Request ---')
244+
245+
// For dataSetId=0 (create new), we need both CreateDataSet and AddPieces signatures
246+
const authHelper = new PDPAuthHelper(fwssAddress, signer, BigInt(synapse.getChainId()))
247+
const clientDataSetId = 0n // New dataset
248+
const nonce = randU256()
249+
const pieceCids = uploadResults.map((r) => r.pieceCid)
250+
const datasetMetadata = [] // Empty metadata for dataset
251+
const pieceMetadata = uploadResults.map(() => []) // Empty metadata for each piece
252+
253+
console.log(`Client: ${address}`)
254+
console.log(`Payee (SP2): ${sp2Info.serviceProvider}`)
255+
console.log(`Client Dataset ID: ${clientDataSetId}`)
256+
console.log(`Nonce: ${nonce}`)
257+
console.log(`PieceCIDs: ${pieceCids.length} pieces`)
258+
for (const cid of pieceCids) {
259+
console.log(` - ${cid.slice(0, 40)}...`)
260+
}
261+
262+
// Sign CreateDataSet (authorizes creating a new dataset with SP2 as payee)
263+
console.log(`\nSigning CreateDataSet...`)
264+
const createAuthData = await authHelper.signCreateDataSet(clientDataSetId, sp2Info.serviceProvider, datasetMetadata)
265+
console.log(` CreateDataSet signature: ${createAuthData.signature.slice(0, 20)}...`)
266+
267+
// Sign AddPieces (authorizes adding these pieces to the dataset)
268+
console.log(`Signing AddPieces...`)
269+
const addAuthData = await authHelper.signAddPieces(clientDataSetId, nonce, pieceCids, pieceMetadata)
270+
console.log(` AddPieces signature: ${addAuthData.signature.slice(0, 20)}...`)
271+
272+
// Encode CreateDataSet extraData
273+
const createExtraData = encodeCreateDataSetExtraData(
274+
address, // payer
275+
clientDataSetId,
276+
datasetMetadata,
277+
createAuthData.signature
278+
)
279+
280+
// Encode AddPieces extraData
281+
const addExtraData = encodeAddPiecesExtraData(nonce, pieceMetadata, addAuthData.signature)
282+
283+
// Combine for dataSetId=0 case
284+
const extraData = encodeCombinedExtraData(createExtraData, addExtraData)
285+
console.log(` Combined extraData encoded (${extraData.length} chars)`)
286+
287+
// Initiate fetch from SP2
288+
console.log('\n--- Initiating Fetch from SP2 ---')
289+
console.log(`Target SP2 URL: ${sp2Url}`)
290+
console.log(`Requesting SP2 to fetch ${uploadResults.length} piece(s) from SP1...`)
291+
292+
// Build pieces array with source URLs for each piece
293+
const piecesToFetch = uploadResults.map(({ pieceCid }) => ({
294+
pieceCid: pieceCid,
295+
sourceUrl: `${sp1Url}/piece/${pieceCid}`,
296+
}))
297+
298+
const fetchResult = await spFetch.pollStatus({
299+
endpoint: sp2Url,
300+
recordKeeper: fwssAddress,
301+
extraData: extraData,
302+
dataSetId: 0n, // Create new (for validation only)
303+
pieces: piecesToFetch,
304+
onStatus: (response) => {
305+
console.log(` Fetch status: ${response.status}`)
306+
for (const piece of response.pieces) {
307+
console.log(` ${piece.pieceCid.slice(0, 20)}...: ${piece.status}`)
308+
}
309+
},
310+
minTimeout: 2000, // Poll every 2 seconds
311+
})
312+
313+
console.log(`\nFetch completed with status: ${fetchResult.status}`)
314+
315+
if (fetchResult.status === 'complete') {
316+
console.log('\n--- Verifying SP2 has all pieces ---')
317+
318+
let allMatched = true
319+
for (const { filePath, pieceCid } of uploadResults) {
320+
const sp2PieceUrl = `${sp2Url}/piece/${pieceCid}`
321+
console.log(`\nDownloading ${pieceCid.slice(0, 30)}... from SP2`)
322+
323+
const downloadResponse = await fetch(sp2PieceUrl)
324+
if (downloadResponse.ok) {
325+
const downloadedData = await downloadResponse.arrayBuffer()
326+
console.log(` Downloaded ${formatBytes(downloadedData.byteLength)}`)
327+
328+
// Compare with original file
329+
const originalData = await fsPromises.readFile(filePath)
330+
const matches = Buffer.from(originalData).equals(Buffer.from(downloadedData))
331+
332+
if (matches) {
333+
console.log(` MATCH: ${filePath}`)
334+
} else {
335+
console.error(` MISMATCH: ${filePath}`)
336+
allMatched = false
337+
}
338+
} else {
339+
console.error(` ERROR: Failed to download: ${downloadResponse.status}`)
340+
const errorText = await downloadResponse.text()
341+
console.error(` Response: ${errorText}`)
342+
allMatched = false
343+
}
344+
}
345+
346+
if (allMatched) {
347+
console.log(`\nSUCCESS: All ${uploadResults.length} pieces verified on SP2!`)
348+
} else {
349+
console.error('\nERROR: Some pieces did not match!')
350+
process.exit(1)
351+
}
352+
} else if (fetchResult.status === 'failed') {
353+
console.error('\nERROR: Fetch failed!')
354+
for (const piece of fetchResult.pieces) {
355+
console.error(` ${piece.pieceCid}: ${piece.status}`)
356+
}
357+
process.exit(1)
358+
}
359+
360+
console.log('\n=== SP-to-SP Fetch Test Complete ===')
361+
} catch (error) {
362+
console.error('\nERROR:', error.message)
363+
if (error.cause) {
364+
console.error('Caused by:', error.cause.message)
365+
}
366+
console.error(error)
367+
process.exit(1)
368+
}
369+
}
370+
371+
// Run the test
372+
main().catch(console.error)

0 commit comments

Comments
 (0)