Skip to content

Commit 8b83a02

Browse files
SgtPookirvagg
andauthored
feat: add data-set command (#50)
* feat: make WarmStorage approvals infinite, focus only on deposit Remove all of the complexity of dealing with approvals and rate limits by just setting them to be infinite by default for WarmStorage (i.e. it's a trusted service and your risk is managed by deposit). This lets us trim down the UI for dealing with payments and simplifies the messaging. * feat: add data-set command * fix: only display data-sets created by filecoin-pin * feat: list by default if no datasetId given * chore: fix lint * chore: normalize data-set command to match others * test: add basic data-set tests * fix: prevent duplicate dataset list with [id] --ls * feat: more service details for dataset * chore: remove pointless Object.create(null) --------- Co-authored-by: Rod Vagg <[email protected]>
1 parent 1064d78 commit 8b83a02

File tree

6 files changed

+909
-0
lines changed

6 files changed

+909
-0
lines changed

src/cli.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { fileURLToPath } from 'node:url'
55

66
import { Command } from 'commander'
77
import { addCommand } from './commands/add.js'
8+
import { dataSetCommand } from './commands/data-set.js'
89
import { importCommand } from './commands/import.js'
910
import { paymentsCommand } from './commands/payments.js'
1011
import { serverCommand } from './commands/server.js'
@@ -23,6 +24,7 @@ const program = new Command()
2324
// Add subcommands
2425
program.addCommand(serverCommand)
2526
program.addCommand(paymentsCommand)
27+
program.addCommand(dataSetCommand)
2628
program.addCommand(importCommand)
2729
program.addCommand(addCommand)
2830

src/commands/data-set.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { RPC_URLS } from '@filoz/synapse-sdk'
2+
import { Command } from 'commander'
3+
import { runDataSetCommand } from '../data-set/run.js'
4+
import type { DataSetCommandOptions } from '../data-set/types.js'
5+
6+
export const dataSetCommand = new Command('data-set')
7+
.description('Inspect data sets managed through Filecoin Onchain Cloud')
8+
.argument('[dataSetId]', 'Optional data set ID to inspect')
9+
.option('--ls', 'List all data sets for the configured account')
10+
.option('--private-key <key>', 'Private key (or PRIVATE_KEY env)')
11+
.option('--rpc-url <url>', 'RPC endpoint (or RPC_URL env)', RPC_URLS.calibration.websocket)
12+
.action(async (dataSetId: string | undefined, options) => {
13+
try {
14+
const commandOptions: DataSetCommandOptions = {
15+
ls: options.ls,
16+
privateKey: options.privateKey || process.env.PRIVATE_KEY,
17+
rpcUrl: options.rpcUrl || process.env.RPC_URL,
18+
}
19+
20+
await runDataSetCommand(dataSetId, commandOptions)
21+
} catch (error) {
22+
console.error('Data set command failed:', error instanceof Error ? error.message : error)
23+
process.exit(1)
24+
}
25+
})

src/data-set/inspect.ts

Lines changed: 285 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,285 @@
1+
import { METADATA_KEYS } from '@filoz/synapse-sdk'
2+
import { ethers } from 'ethers'
3+
import pc from 'picocolors'
4+
import { formatFileSize } from '../utils/cli-helpers.js'
5+
import { log } from '../utils/cli-logger.js'
6+
import type { DataSetDetail, DataSetInspectionContext, PieceDetail } from './types.js'
7+
8+
/**
9+
* Convert dataset lifecycle information into a coloured status label.
10+
*/
11+
function statusLabel(dataSet: DataSetDetail['base']): string {
12+
if (dataSet.isLive) {
13+
return pc.green('live')
14+
}
15+
16+
if (dataSet.pdpEndEpoch > 0) {
17+
return pc.red(`terminated @ epoch ${dataSet.pdpEndEpoch}`)
18+
}
19+
20+
return pc.yellow('inactive')
21+
}
22+
23+
function providerLabel(provider: DataSetDetail['provider'], dataSet: DataSetDetail['base']): string {
24+
if (provider != null && provider.name.trim() !== '') {
25+
return `${provider.name} (ID ${provider.id})`
26+
}
27+
28+
return `${dataSet.serviceProvider} (ID ${dataSet.providerId})`
29+
}
30+
31+
function formatCommission(commissionBps: number): string {
32+
const percent = commissionBps / 100
33+
return `${percent.toFixed(2)}%`
34+
}
35+
36+
function formatBytes(value?: bigint): string {
37+
if (value == null) {
38+
return pc.gray('unknown')
39+
}
40+
41+
if (value <= BigInt(Number.MAX_SAFE_INTEGER)) {
42+
return formatFileSize(Number(value))
43+
}
44+
45+
return `${value.toString()} B`
46+
}
47+
48+
/**
49+
* Format payment token address for display
50+
*/
51+
function formatPaymentToken(tokenAddress: string): string {
52+
// Zero address typically means native token (FIL) or USDFC
53+
if (tokenAddress === '0x0000000000000000000000000000000000000000') {
54+
return `USDFC ${pc.gray('(native)')}`
55+
}
56+
57+
// For other addresses, show a truncated version
58+
return `${tokenAddress.slice(0, 6)}...${tokenAddress.slice(-4)}`
59+
}
60+
61+
/**
62+
* Format storage price in USDFC per TiB per month
63+
* Always shows TiB/month for consistency, with appropriate precision
64+
*/
65+
function formatStoragePrice(pricePerTiBPerMonth: bigint): string {
66+
try {
67+
const priceInUSDFC = parseFloat(ethers.formatUnits(pricePerTiBPerMonth, 18))
68+
69+
// Handle very small prices that would show as 0.0000
70+
if (priceInUSDFC < 0.0001) {
71+
return '< 0.0001 USDFC/TiB/month'
72+
}
73+
74+
// For prices >= 0.0001, show with appropriate precision
75+
if (priceInUSDFC >= 1) {
76+
return `${priceInUSDFC.toFixed(2)} USDFC/TiB/month`
77+
} else if (priceInUSDFC >= 0.01) {
78+
return `${priceInUSDFC.toFixed(4)} USDFC/TiB/month`
79+
} else {
80+
return `${priceInUSDFC.toFixed(6)} USDFC/TiB/month`
81+
}
82+
} catch {
83+
return pc.red('invalid price')
84+
}
85+
}
86+
87+
/**
88+
* Render metadata key-value pairs with consistent indentation.
89+
*/
90+
function renderMetadata(metadata: Record<string, string>, indentLevel: number = 1): void {
91+
const entries = Object.entries(metadata)
92+
if (entries.length === 0) {
93+
log.indent(pc.gray('none'), indentLevel)
94+
return
95+
}
96+
97+
for (const [key, value] of entries) {
98+
const displayValue = value === '' ? pc.gray('(empty)') : value
99+
log.indent(`${key}: ${displayValue}`, indentLevel)
100+
}
101+
}
102+
103+
/**
104+
* Render a single piece entry including CommP, root CID, and extra metadata.
105+
*/
106+
function renderPiece(piece: PieceDetail, baseIndentLevel: number = 2): void {
107+
const rootCid = piece.metadata[METADATA_KEYS.IPFS_ROOT_CID]
108+
const rootDisplay = rootCid ?? pc.gray('unknown')
109+
110+
log.indent(`#${piece.pieceId}`, baseIndentLevel)
111+
log.indent(`CommP: ${piece.pieceCid}`, baseIndentLevel + 1)
112+
log.indent(`Root CID: ${rootDisplay}`, baseIndentLevel + 1)
113+
114+
const extraMetadataEntries = Object.entries(piece.metadata).filter(([key]) => key !== METADATA_KEYS.IPFS_ROOT_CID)
115+
116+
if (extraMetadataEntries.length > 0) {
117+
log.indent('Metadata:', baseIndentLevel + 1)
118+
for (const [key, value] of extraMetadataEntries) {
119+
const displayValue = value === '' ? pc.gray('(empty)') : value
120+
log.indent(`${key}: ${displayValue}`, baseIndentLevel + 2)
121+
}
122+
}
123+
}
124+
125+
/**
126+
* Print the lightweight dataset list used for the default command output.
127+
*/
128+
export function displayDataSetList(ctx: DataSetInspectionContext): void {
129+
log.line(`Address: ${ctx.address}`)
130+
log.line(`Network: ${pc.bold(ctx.network)}`)
131+
log.line('')
132+
133+
if (ctx.dataSets.length === 0) {
134+
log.line(pc.yellow('No data sets managed by filecoin-pin were found for this account.'))
135+
log.flush()
136+
return
137+
}
138+
139+
const ordered = [...ctx.dataSets].sort((a, b) => a.base.pdpVerifierDataSetId - b.base.pdpVerifierDataSetId)
140+
141+
for (const dataSet of ordered) {
142+
const { base, provider } = dataSet
143+
const annotations: string[] = []
144+
145+
if (base.isManaged) {
146+
annotations.push(pc.gray('managed'))
147+
} else {
148+
annotations.push(pc.yellow('external'))
149+
}
150+
151+
if (base.withCDN) {
152+
annotations.push(pc.cyan('cdn'))
153+
}
154+
155+
log.line(
156+
`${pc.bold(`#${base.pdpVerifierDataSetId}`)}${statusLabel(base)}${
157+
annotations.length > 0 ? ` • ${annotations.join(', ')}` : ''
158+
}`
159+
)
160+
log.indent(`Provider: ${providerLabel(provider, base)}`)
161+
log.indent(`Pieces stored: ${base.currentPieceCount}`)
162+
log.indent(`Leaf count: ${dataSet.leafCount != null ? dataSet.leafCount.toString() : pc.gray('unknown')}`)
163+
log.indent(`Total size: ${formatBytes(dataSet.totalSizeBytes)}`)
164+
log.indent(`Client data set ID: ${base.clientDataSetId}`)
165+
log.indent(`PDP rail ID: ${base.pdpRailId}`)
166+
log.indent(`CDN rail ID: ${base.cdnRailId > 0 ? base.cdnRailId : 'none'}`)
167+
log.indent(`Cache-miss rail ID: ${base.cacheMissRailId > 0 ? base.cacheMissRailId : 'none'}`)
168+
log.indent(`Payer: ${base.payer}`)
169+
log.indent(`Payee: ${base.payee}`)
170+
log.line('')
171+
172+
log.indent(pc.bold('Metadata'))
173+
renderMetadata(dataSet.metadata, 2)
174+
log.line('')
175+
176+
if (dataSet.warnings.length > 0) {
177+
log.indent(pc.bold(pc.yellow('Warnings')))
178+
for (const warning of dataSet.warnings) {
179+
log.indent(pc.yellow(`- ${warning}`), 2)
180+
}
181+
log.line('')
182+
}
183+
184+
log.indent(pc.bold('Pieces'))
185+
if (dataSet.pieces.length === 0) {
186+
log.indent(pc.gray('No piece information available'), 2)
187+
} else {
188+
for (const piece of dataSet.pieces) {
189+
renderPiece(piece, 2)
190+
}
191+
}
192+
193+
log.line('')
194+
}
195+
196+
log.flush()
197+
}
198+
199+
/**
200+
* Render detailed information for a single dataset.
201+
*
202+
* @returns true when the dataset exists; false otherwise.
203+
*/
204+
export function displayDataSetStatus(ctx: DataSetInspectionContext, dataSetId: number): boolean {
205+
const dataSet = ctx.dataSets.find((item) => item.base.pdpVerifierDataSetId === dataSetId)
206+
if (dataSet == null) {
207+
log.line(pc.red(`No data set found with ID ${dataSetId}`))
208+
log.flush()
209+
return false
210+
}
211+
212+
const { base, provider } = dataSet
213+
214+
log.line(`${pc.bold(`Data Set #${base.pdpVerifierDataSetId}`)}${statusLabel(base)}`)
215+
log.indent(`Managed by Warm Storage: ${base.isManaged ? 'yes' : 'no'}`)
216+
log.indent(`CDN add-on: ${base.withCDN ? 'enabled' : 'disabled'}`)
217+
log.indent(`Pieces stored: ${base.currentPieceCount}`)
218+
log.indent(`Leaf count: ${dataSet.leafCount != null ? dataSet.leafCount.toString() : pc.gray('unknown')}`)
219+
log.indent(`Total size: ${formatBytes(dataSet.totalSizeBytes)}`)
220+
log.indent(`Client data set ID: ${base.clientDataSetId}`)
221+
log.indent(`PDP rail ID: ${base.pdpRailId}`)
222+
log.indent(`CDN rail ID: ${base.cdnRailId > 0 ? base.cdnRailId : 'none'}`)
223+
log.indent(`Cache-miss rail ID: ${base.cacheMissRailId > 0 ? base.cacheMissRailId : 'none'}`)
224+
log.indent(`Payer: ${base.payer}`)
225+
log.indent(`Payee: ${base.payee}`)
226+
log.indent(`Service provider: ${base.serviceProvider}`)
227+
log.indent(`Provider: ${providerLabel(provider, base)}`)
228+
log.indent(`Commission: ${formatCommission(base.commissionBps)}`)
229+
230+
// Add provider service information
231+
if (provider?.products?.PDP?.data) {
232+
const pdpData = provider.products.PDP.data
233+
log.line('')
234+
log.line(pc.bold('Provider Service'))
235+
log.indent(`Service URL: ${pdpData.serviceURL}`)
236+
log.indent(`Min piece size: ${formatBytes(BigInt(pdpData.minPieceSizeInBytes))}`)
237+
log.indent(`Max piece size: ${formatBytes(BigInt(pdpData.maxPieceSizeInBytes))}`)
238+
log.indent(`Storage price: ${formatStoragePrice(pdpData.storagePricePerTibPerMonth)}`)
239+
log.indent(`Min proving period: ${pdpData.minProvingPeriodInEpochs} epochs`)
240+
log.indent(`Location: ${pdpData.location}`)
241+
log.indent(`Payment token: ${formatPaymentToken(pdpData.paymentTokenAddress)}`)
242+
}
243+
244+
if (base.pdpEndEpoch > 0) {
245+
log.indent(pc.yellow(`PDP payments ended @ epoch ${base.pdpEndEpoch}`))
246+
}
247+
if (base.cdnEndEpoch > 0) {
248+
log.indent(pc.yellow(`CDN payments ended @ epoch ${base.cdnEndEpoch}`))
249+
}
250+
251+
log.line('')
252+
log.line(pc.bold('Metadata'))
253+
renderMetadata(dataSet.metadata, 2)
254+
log.line('')
255+
256+
if (dataSet.warnings.length > 0) {
257+
log.line(pc.bold(pc.yellow('Warnings')))
258+
for (const warning of dataSet.warnings) {
259+
log.indent(pc.yellow(`- ${warning}`))
260+
}
261+
log.line('')
262+
}
263+
264+
log.line('')
265+
log.line(pc.bold('Pieces'))
266+
if (dataSet.pieces.length === 0) {
267+
log.indent(pc.gray('No piece information available'))
268+
} else {
269+
// Show piece summary
270+
const uniqueCommPs = new Set(dataSet.pieces.map((p) => p.pieceCid))
271+
const uniqueRootCids = new Set(dataSet.pieces.map((p) => p.metadata[METADATA_KEYS.IPFS_ROOT_CID]).filter(Boolean))
272+
273+
log.indent(`Total pieces: ${dataSet.pieces.length}`)
274+
log.indent(`Unique CommPs: ${uniqueCommPs.size}`)
275+
log.indent(`Unique root CIDs: ${uniqueRootCids.size}`)
276+
log.line('')
277+
278+
for (const piece of dataSet.pieces) {
279+
renderPiece(piece, 1)
280+
}
281+
}
282+
283+
log.flush()
284+
return true
285+
}

0 commit comments

Comments
 (0)