Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions packages/synapse-core/src/sp-registry/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,4 +71,10 @@ export interface PDPOffering {
* The IPNI peer ID.
*/
ipniPeerId?: string
/**
* Additional non-standard capabilities declared by the provider.
* Keys are capability names as registered on-chain (plain strings),
* values are the raw hex-encoded bytes from the contract.
*/
extraCapabilities?: Record<string, Hex>
}
39 changes: 26 additions & 13 deletions packages/synapse-core/src/utils/pdp-capabilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,20 @@ import { zHex } from './schemas.ts'
*
* @see https://github.com/FilOzone/filecoin-services/blob/a86e4a5018133f17a25b4bb6b5b99da4d34fe664/service_contracts/src/ServiceProviderRegistry.sol#L14
*/
export const PDPOfferingSchema = z.object({
serviceURL: zHex,
minPieceSizeInBytes: zHex,
maxPieceSizeInBytes: zHex,
storagePricePerTibPerDay: zHex,
minProvingPeriodInEpochs: zHex,
location: zHex,
paymentTokenAddress: zHex,
ipniPiece: zHex.optional(),
ipniIpfs: zHex.optional(),
ipniPeerId: zHex.optional(),
})
export const PDPOfferingSchema = z
.object({
serviceURL: zHex,
minPieceSizeInBytes: zHex,
maxPieceSizeInBytes: zHex,
storagePricePerTibPerDay: zHex,
minProvingPeriodInEpochs: zHex,
location: zHex,
paymentTokenAddress: zHex,
ipniPiece: zHex.optional(),
ipniIpfs: zHex.optional(),
ipniPeerId: zHex.optional(),
})
.catchall(zHex)
// Standard capability keys for PDP product type (must match ServiceProviderRegistry.sol REQUIRED_PDP_KEYS)
export const CAP_SERVICE_URL = 'serviceURL'
export const CAP_MIN_PIECE_SIZE = 'minPieceSizeInBytes'
Expand All @@ -47,6 +49,9 @@ export function decodePDPOffering(provider: ProviderWithProduct): PDPOffering {
return decodePDPCapabilities(parsed.data)
}

/** Capability keys that are decoded into typed PDPOffering fields, derived from the schema */
const KNOWN_CAPABILITY_KEYS = new Set([...Object.keys(PDPOfferingSchema.shape), CAP_IPNI_PEER_ID_LEGACY])
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This array likely becomes a maintenance burden, and is hard to test in integration.

Would it be easier to expose all capabilities in one object, and not differentiate standard and non-standard capabilities?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that'd be a downgrade, standard fields give us a well formed struct with proper decoding, flattening them would mean a Record<string, something> bucket to go fishing through. At least we can type the known keys and they match the contract; we can expect to find required keys and may expect to find the non-required but known keys, the rest are a wild west.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some examples of where we'd lose fidelity: provider.pdp.minPieceSizeInBytes (and max) are decoded as bigint, provider.pdp.paymentTokenAddress as Address, provider.pdp.ipniPiece / ipniIpfs as boolean; we know this and when they don't decode like this something is wrong, and it also means we can offer a typed struct for consumers of known fields.


/**
* Decode PDP capabilities from keys/values arrays into a PDPOffering object.
* Based on Curio's capabilitiesToOffering function.
Expand All @@ -71,7 +76,15 @@ export function decodePDPCapabilities(capabilities: Record<string, Hex>): PDPOff
? base58btc.encode(fromHex(capabilities[CAP_IPNI_PEER_ID_LEGACY], 'bytes'))
: undefined,
}
return { ...required, ...optional }

const extraCapabilities: Record<string, Hex> = Object.create(null)
for (const key of Object.keys(capabilities)) {
if (!KNOWN_CAPABILITY_KEYS.has(key)) {
extraCapabilities[key] = capabilities[key]
}
}

return { ...required, ...optional, extraCapabilities }
}

/**
Expand Down
37 changes: 37 additions & 0 deletions packages/synapse-core/test/pdp-capabilities.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,43 @@ describe('decodePDPCapabilities', () => {
assert.strictEqual(result.ipniPeerId, undefined)
})
})

describe('extraCapabilities', () => {
it('preserves non-standard capabilities in extraCapabilities', () => {
const capabilities = createMinimalCapabilities({
serviceStatus: toHex('dev'),
customFlag: '0x01',
})

const result = decodePDPCapabilities(capabilities)

assert.ok(result.extraCapabilities)
assert.strictEqual(result.extraCapabilities.serviceStatus, toHex('dev'))
assert.strictEqual(result.extraCapabilities.customFlag, '0x01')
})

it('returns empty extraCapabilities when no non-standard capabilities exist', () => {
const capabilities = createMinimalCapabilities()

const result = decodePDPCapabilities(capabilities)

assert.ok(result.extraCapabilities)
assert.strictEqual(Object.keys(result.extraCapabilities).length, 0)
})

it('does not include standard capabilities in extraCapabilities', () => {
const capabilities = createMinimalCapabilities({
serviceStatus: toHex('dev'),
})

const result = decodePDPCapabilities(capabilities)

assert.ok(result.extraCapabilities)
assert.strictEqual(Object.keys(result.extraCapabilities).length, 1)
assert.strictEqual(result.extraCapabilities.serviceURL, undefined)
assert.strictEqual(result.extraCapabilities.location, undefined)
})
})
})

// Minimal valid capabilities for testing (all required fields)
Expand Down
1 change: 1 addition & 0 deletions packages/synapse-sdk/src/test/storage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1270,6 +1270,7 @@ describe('StorageService', () => {
minProvingPeriodInEpochs: 30n,
location: 'us-east',
paymentTokenAddress: '0xb3042734b608a1b16e9e86b374a3f3e389b4cdf0',
extraCapabilities: {},
},
})
})
Expand Down