@filoz/repair-cli 0.4.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/dist/package.json +4 -4
  2. package/dist/src/api.d.ts +35 -0
  3. package/dist/src/api.d.ts.map +1 -0
  4. package/dist/src/api.js +118 -0
  5. package/dist/src/api.js.map +1 -0
  6. package/dist/src/commands/datasets.d.ts +5 -7
  7. package/dist/src/commands/datasets.d.ts.map +1 -1
  8. package/dist/src/commands/datasets.js +33 -35
  9. package/dist/src/commands/datasets.js.map +1 -1
  10. package/dist/src/commands/providers.d.ts +5 -7
  11. package/dist/src/commands/providers.d.ts.map +1 -1
  12. package/dist/src/commands/providers.js +32 -33
  13. package/dist/src/commands/providers.js.map +1 -1
  14. package/dist/src/commands/repair.d.ts +5 -7
  15. package/dist/src/commands/repair.d.ts.map +1 -1
  16. package/dist/src/commands/repair.js +5 -1
  17. package/dist/src/commands/repair.js.map +1 -1
  18. package/dist/src/commands/replicate.d.ts +5 -7
  19. package/dist/src/commands/replicate.d.ts.map +1 -1
  20. package/dist/src/commands/replicate.js +5 -1
  21. package/dist/src/commands/replicate.js.map +1 -1
  22. package/dist/src/commands/second-copy.d.ts +5 -7
  23. package/dist/src/commands/second-copy.d.ts.map +1 -1
  24. package/dist/src/commands/second-copy.js +5 -1
  25. package/dist/src/commands/second-copy.js.map +1 -1
  26. package/dist/src/commands/session-key.d.ts +5 -7
  27. package/dist/src/commands/session-key.d.ts.map +1 -1
  28. package/dist/src/commands/setup.d.ts.map +1 -1
  29. package/dist/src/commands/setup.js +11 -32
  30. package/dist/src/commands/setup.js.map +1 -1
  31. package/dist/src/commands/wallet.d.ts +5 -7
  32. package/dist/src/commands/wallet.d.ts.map +1 -1
  33. package/dist/src/db/dedupe-cids.d.ts +6 -7
  34. package/dist/src/db/dedupe-cids.d.ts.map +1 -1
  35. package/dist/src/db/dedupe-cids.js +2 -14
  36. package/dist/src/db/dedupe-cids.js.map +1 -1
  37. package/dist/src/db/find-repair-dataset.d.ts +3 -3
  38. package/dist/src/db/find-repair-dataset.d.ts.map +1 -1
  39. package/dist/src/db/find-repair-dataset.js +16 -14
  40. package/dist/src/db/find-repair-dataset.js.map +1 -1
  41. package/dist/src/db/get-active-data-set-ids.d.ts +9 -0
  42. package/dist/src/db/get-active-data-set-ids.d.ts.map +1 -0
  43. package/dist/src/db/get-active-data-set-ids.js +9 -0
  44. package/dist/src/db/get-active-data-set-ids.js.map +1 -0
  45. package/dist/src/db/get-alternate-provider-urls.d.ts +6 -0
  46. package/dist/src/db/get-alternate-provider-urls.d.ts.map +1 -0
  47. package/dist/src/db/get-alternate-provider-urls.js +24 -0
  48. package/dist/src/db/get-alternate-provider-urls.js.map +1 -0
  49. package/dist/src/db/get-pieces.d.ts +8 -27
  50. package/dist/src/db/get-pieces.d.ts.map +1 -1
  51. package/dist/src/db/get-pieces.js +44 -97
  52. package/dist/src/db/get-pieces.js.map +1 -1
  53. package/dist/src/db/get-repair-provider.d.ts +2 -2
  54. package/dist/src/db/get-repair-provider.d.ts.map +1 -1
  55. package/dist/src/db/get-repair-provider.js +8 -20
  56. package/dist/src/db/get-repair-provider.js.map +1 -1
  57. package/dist/src/db/repair-create.js +3 -3
  58. package/dist/src/db/repair-create.js.map +1 -1
  59. package/dist/src/db/replicate-create.d.ts.map +1 -1
  60. package/dist/src/db/replicate-create.js +13 -21
  61. package/dist/src/db/replicate-create.js.map +1 -1
  62. package/dist/src/db/second-copy-create.js +2 -2
  63. package/dist/src/db/second-copy-create.js.map +1 -1
  64. package/dist/src/local-schema.d.ts +1 -1
  65. package/dist/src/middleware.d.ts +12 -16
  66. package/dist/src/middleware.d.ts.map +1 -1
  67. package/dist/src/middleware.js +6 -10
  68. package/dist/src/middleware.js.map +1 -1
  69. package/dist/src/pipeline/add-pieces.d.ts +4 -4
  70. package/dist/src/pipeline/add-pieces.d.ts.map +1 -1
  71. package/dist/src/pipeline/add-pieces.js +32 -19
  72. package/dist/src/pipeline/add-pieces.js.map +1 -1
  73. package/dist/src/pipeline/create-datasets.d.ts +4 -4
  74. package/dist/src/pipeline/create-datasets.d.ts.map +1 -1
  75. package/dist/src/pipeline/create-datasets.js +7 -14
  76. package/dist/src/pipeline/create-datasets.js.map +1 -1
  77. package/dist/src/types.d.ts +8 -9
  78. package/dist/src/types.d.ts.map +1 -1
  79. package/dist/src/utils.d.ts +45 -37
  80. package/dist/src/utils.d.ts.map +1 -1
  81. package/dist/src/utils.js +47 -2
  82. package/dist/src/utils.js.map +1 -1
  83. package/package.json +4 -4
  84. package/readme.md +22 -19
  85. package/src/api.ts +220 -0
  86. package/src/commands/datasets.ts +33 -38
  87. package/src/commands/providers.ts +34 -42
  88. package/src/commands/repair.ts +5 -1
  89. package/src/commands/replicate.ts +5 -1
  90. package/src/commands/second-copy.ts +5 -1
  91. package/src/commands/setup.ts +12 -35
  92. package/src/db/dedupe-cids.ts +7 -24
  93. package/src/db/find-repair-dataset.ts +17 -28
  94. package/src/db/get-active-data-set-ids.ts +29 -0
  95. package/src/db/get-alternate-provider-urls.ts +43 -0
  96. package/src/db/get-pieces.ts +82 -214
  97. package/src/db/get-repair-provider.ts +10 -30
  98. package/src/db/repair-create.ts +3 -3
  99. package/src/db/replicate-create.ts +13 -21
  100. package/src/db/second-copy-create.ts +2 -2
  101. package/src/middleware.ts +10 -14
  102. package/src/pipeline/add-pieces.ts +52 -24
  103. package/src/pipeline/create-datasets.ts +9 -29
  104. package/src/types.ts +11 -9
  105. package/src/utils.ts +71 -7
  106. package/dist/src/db/find-providers-by-cid.d.ts +0 -9
  107. package/dist/src/db/find-providers-by-cid.d.ts.map +0 -1
  108. package/dist/src/db/find-providers-by-cid.js +0 -43
  109. package/dist/src/db/find-providers-by-cid.js.map +0 -1
  110. package/dist/src/indexer-schema.d.ts +0 -549
  111. package/dist/src/indexer-schema.d.ts.map +0 -1
  112. package/dist/src/indexer-schema.js +0 -56
  113. package/dist/src/indexer-schema.js.map +0 -1
  114. package/src/db/find-providers-by-cid.ts +0 -70
  115. package/src/indexer-schema.ts +0 -77
package/readme.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  The `repair` CLI helps prepare and run repair and replication jobs on Filecoin PDP datasets. It uses:
8
8
 
9
- - an indexer Postgres database as the read-only source of providers, datasets, and pieces
9
+ - the [Filecoin API](https://github.com/filecoin-project/agents/tree/main/apps/filecoin-api) (`https://api.filecoin.io` by default) as the read-only source of providers, datasets, and pieces
10
10
  - a local SQLite database to track repair and replication jobs and per-piece operations
11
11
  - a configured Filecoin wallet to create datasets and submit on-chain add-piece transactions
12
12
 
@@ -24,7 +24,7 @@ repair --help
24
24
 
25
25
  ## Setup
26
26
 
27
- Run setup before using any command that talks to the indexer, local database, or wallet.
27
+ Run setup before using any command that talks to the Filecoin API, local database, or wallet.
28
28
 
29
29
  ```bash
30
30
  repair setup
@@ -33,8 +33,7 @@ repair setup
33
33
  Setup prompts for:
34
34
 
35
35
  - private key for the repair wallet
36
- - mainnet indexer Postgres URL
37
- - calibration indexer Postgres URL
36
+ - Filecoin API URL (defaults to `https://api.filecoin.io`)
38
37
  - chain, either Filecoin Mainnet `314` or Filecoin Calibration `314159`
39
38
  - local SQLite database path
40
39
  - dataset source label, used when creating repair target datasets (defaults to `early-repair`)
@@ -59,7 +58,7 @@ Interactive configuration and local database setup.
59
58
  repair setup
60
59
  ```
61
60
 
62
- Use this whenever you need to initialize the CLI, change the active chain, update indexer URLs, change the dataset source label, or move the local SQLite database.
61
+ Use this whenever you need to initialize the CLI, change the active chain, change the Filecoin API URL, change the dataset source label, or move the local SQLite database.
63
62
 
64
63
  ### `repair wallet fund`
65
64
 
@@ -136,7 +135,7 @@ The command waits for the revoke transaction to be mined and returns the session
136
135
 
137
136
  ### `repair providers list`
138
137
 
139
- Lists PDP providers from the configured indexer.
138
+ Lists PDP providers from the Filecoin API.
140
139
 
141
140
  ```bash
142
141
  repair providers list
@@ -149,8 +148,6 @@ By default, the command returns active PDP providers that are approved or endors
149
148
  - `serviceUrl`
150
149
  - `approved`
151
150
  - `endorsed`
152
- - `pieceCount`, the number of active indexed pieces for that provider
153
- - `totalSize`, the sum of active piece raw sizes formatted in decimal GB
154
151
 
155
152
  Use `--all` to include every active PDP provider, even if it is not approved or endorsed.
156
153
 
@@ -158,6 +155,12 @@ Use `--all` to include every active PDP provider, even if it is not approved or
158
155
  repair providers list --all
159
156
  ```
160
157
 
158
+ Use `--stats` to also return `pieceCount`, the number of active indexed pieces for each provider, and `totalSize`, the sum of their raw sizes in decimal GB. The API has no aggregate endpoint, so this pages through every piece of every listed provider and can take a long time.
159
+
160
+ ```bash
161
+ repair providers list --stats
162
+ ```
163
+
161
164
  ### `repair datasets list`
162
165
 
163
166
  Lists datasets owned by the configured repair wallet.
@@ -166,7 +169,7 @@ Lists datasets owned by the configured repair wallet.
166
169
  repair datasets list
167
170
  ```
168
171
 
169
- Each dataset includes its ID, CDN/IPFS indexing flags, source, provider URL, PDP end epoch, and piece count.
172
+ Each dataset includes its ID, CDN/IPFS indexing flags, source, provider URL, and PDP end epoch. Add `--stats` to include each dataset's active piece count, which pages through every piece.
170
173
 
171
174
  Filter by provider ID:
172
175
 
@@ -176,7 +179,7 @@ repair datasets list --provider-id 123
176
179
 
177
180
  ### `repair datasets show <id>`
178
181
 
179
- Shows a dataset from the configured indexer.
182
+ Shows a dataset from the Filecoin API.
180
183
 
181
184
  ```bash
182
185
  repair datasets show 42
@@ -206,7 +209,7 @@ repair repair create --provider-id 101 --target-provider-id 202
206
209
 
207
210
  `--target-provider-id` is the provider that should receive the repaired pieces. It must be different from `--provider-id`.
208
211
 
209
- The command snapshots the current chain block number, creates a local repair row, scans active pieces for the source provider, deduplicates them by CID, and creates local `add_piece` operations. Pieces with no alternate provider are marked `skipped`. The command returns a `repairId`.
212
+ The command snapshots the current chain block number, creates a local repair row, scans active pieces for the source provider, deduplicates them by CID, and creates local `add_piece` operations. For each piece it probes the approved and endorsed providers (endorsed first) with `HEAD /piece/{cid}`, then the provider being repaired as a last resort, and uses the first one that serves it as the pull source. Pieces that no provider serves are marked `skipped`. The command returns a `repairId`.
210
213
 
211
214
  ### `repair repair list`
212
215
 
@@ -235,18 +238,18 @@ Runs a pending repair.
235
238
  repair repair run 1
236
239
  ```
237
240
 
238
- The command first ensures the target repair dataset exists for the configured wallet and target provider. If no matching dataset exists, it creates one with IPFS indexing enabled and CDN disabled, using the configured source label. Then it processes pending `add_piece` operations by pulling pieces from alternate providers into the target provider and committing them on-chain.
241
+ The command first ensures the target repair dataset exists for the configured wallet and target provider. If no matching dataset exists, it creates one with IPFS indexing enabled and CDN disabled, using the configured source label. Then it reads the CIDs already in the target dataset once, marks matching operations completed, and processes the remaining pending `add_piece` operations by pulling pieces from alternate providers into the target provider and committing them on-chain.
239
242
 
240
243
  Options:
241
244
 
242
245
  - `--concurrency <number>` controls how many pull batches run at once. Defaults to `4`.
243
- - `--batch-size <number>` controls the maximum number of `add_piece` operations per batch. Defaults to `40`.
246
+ - `--batch-size <number>` caps the number of `add_piece` operations per batch. By default each batch holds as many pieces as fit in one `addPieces` message (about 400 without piece metadata, 80 for legacy datasets).
244
247
  - `--payer <address>` overrides the payer address used when creating or finding the target repair dataset. Defaults to the configured wallet address.
245
248
 
246
249
  Example:
247
250
 
248
251
  ```bash
249
- repair repair run 1 --concurrency 8 --batch-size 40
252
+ repair repair run 1 --concurrency 8 --batch-size 100
250
253
  repair repair run 1 --payer 0x1234567890123456789012345678901234567890
251
254
  ```
252
255
 
@@ -304,13 +307,13 @@ The command creates a new target dataset on the target provider, copying CDN and
304
307
  Options:
305
308
 
306
309
  - `--concurrency <number>` controls how many pull batches run at once. Defaults to `4`.
307
- - `--batch-size <number>` controls the maximum number of `add_piece` operations per batch. Defaults to `40`.
310
+ - `--batch-size <number>` caps the number of `add_piece` operations per batch. By default each batch holds as many pieces as fit in one `addPieces` message (about 400 without piece metadata, 80 for legacy datasets).
308
311
  - `--payer <address>` overrides the payer address used when creating the target replication dataset. Defaults to the configured wallet address.
309
312
 
310
313
  Example:
311
314
 
312
315
  ```bash
313
- repair replicate run 1 --concurrency 8 --batch-size 40
316
+ repair replicate run 1 --concurrency 8 --batch-size 100
314
317
  repair replicate run 1 --payer 0x1234567890123456789012345678901234567890
315
318
  ```
316
319
 
@@ -337,7 +340,7 @@ repair second-copy create \
337
340
 
338
341
  `--pieces-file` is a JSON file with an `aggregates` array. Each aggregate must include a `piece_cid` string. Only aggregate `piece_cid` values are used; `sub_pieces` are ignored.
339
342
 
340
- `--source-provider-url` is the provider URL pieces are pulled from. It is not looked up in the indexer, so it can point at a provider on another network (for example Mainnet while the CLI is configured for Calibration).
343
+ `--source-provider-url` is the provider URL pieces are pulled from. It is not looked up in the Filecoin API, so it can point at a provider on another network (for example Mainnet while the CLI is configured for Calibration).
341
344
 
342
345
  `--target-provider-id` is the provider on the configured chain that should receive the copied pieces. Its service URL must differ from `--source-provider-url`.
343
346
 
@@ -373,13 +376,13 @@ The command first ensures the target dataset exists for the configured wallet an
373
376
  Options:
374
377
 
375
378
  - `--concurrency <number>` controls how many pull batches run at once. Defaults to `4`.
376
- - `--batch-size <number>` controls the maximum number of `add_piece` operations per batch. Defaults to `40`.
379
+ - `--batch-size <number>` caps the number of `add_piece` operations per batch. By default each batch holds as many pieces as fit in one `addPieces` message (about 400 without piece metadata, 80 for legacy datasets).
377
380
  - `--payer <address>` overrides the payer address used when creating or finding the target dataset. Defaults to the configured wallet address.
378
381
 
379
382
  Example:
380
383
 
381
384
  ```bash
382
- repair second-copy run 1 --concurrency 8 --batch-size 40
385
+ repair second-copy run 1 --concurrency 8 --batch-size 100
383
386
  repair second-copy run 1 --payer 0x1234567890123456789012345678901234567890
384
387
  ```
385
388
 
package/src/api.ts ADDED
@@ -0,0 +1,220 @@
1
+ import {
2
+ ApiError,
3
+ createApiClient,
4
+ type DataSet,
5
+ DEFAULT_API_URL,
6
+ type Piece,
7
+ type PieceWithDataSet,
8
+ type Provider,
9
+ unwrap,
10
+ } from '@filecoin-project/api-client'
11
+ import { pipeline, withRateLimit, withRetry, withTimeout } from 'fetch-extras'
12
+ import type { IndexerApi } from './types.ts'
13
+
14
+ export { ApiError, DEFAULT_API_URL } from '@filecoin-project/api-client'
15
+ export type { DataSet, Piece, PieceWithDataSet, Provider }
16
+
17
+ /** filecoin-api network path segment. */
18
+ export type Network = 'mainnet' | 'calibration'
19
+
20
+ /** Largest page size filecoin-api accepts. */
21
+ export const MAX_PAGE_SIZE = 200
22
+
23
+ /** Retries for network errors and `408`, `429` and `5xx` responses. */
24
+ const MAX_RETRIES = 5
25
+
26
+ /** Per-attempt request timeout; a timed-out request fails instead of retrying. */
27
+ const REQUEST_TIMEOUT_MS = 30_000
28
+
29
+ /** filecoin-api allows 120 requests per minute per client IP. */
30
+ const RATE_LIMIT = { requestsPerInterval: 120, interval: 60_000 }
31
+
32
+ /** Map a chain ID to its filecoin-api network. */
33
+ export function networkFromChainId(chainId: number): Network {
34
+ return chainId === 314 ? 'mainnet' : 'calibration'
35
+ }
36
+
37
+ /**
38
+ * Create a filecoin-api client for a chain.
39
+ *
40
+ * @param options.baseUrl - API origin; defaults to the client's production URL.
41
+ * @param options.chainId - Chain ID used to pick the network.
42
+ */
43
+ export function createIndexerApi({ baseUrl, chainId }: { baseUrl?: string; chainId: number }): IndexerApi {
44
+ // Pace requests under the API's limit, and still retry 429s (other clients may share the IP),
45
+ // honouring Retry-After.
46
+ const fetchWithRetry = pipeline(
47
+ fetch,
48
+ withTimeout(REQUEST_TIMEOUT_MS),
49
+ withRateLimit(RATE_LIMIT),
50
+ withRetry({ retries: MAX_RETRIES })
51
+ )
52
+ return {
53
+ client: createApiClient({ baseUrl: baseUrl ?? DEFAULT_API_URL, fetch: fetchWithRetry }),
54
+ network: networkFromChainId(chainId),
55
+ }
56
+ }
57
+
58
+ /** Return `null` instead of throwing for a filecoin-api 404. */
59
+ async function orNull<T>(promise: Promise<T>): Promise<T | null> {
60
+ try {
61
+ return await promise
62
+ } catch (error) {
63
+ if (ApiError.isNotFound(error)) return null
64
+ throw error
65
+ }
66
+ }
67
+
68
+ /** Collect every item from a cursor-paginated walk. */
69
+ async function collect<T>(pages: AsyncIterable<T[]>): Promise<T[]> {
70
+ const items: T[] = []
71
+ for await (const page of pages) items.push(...page)
72
+ return items
73
+ }
74
+
75
+ /**
76
+ * Walk a cursor-paginated list, yielding one page of items at a time.
77
+ *
78
+ * @param fetchPage - Fetches the page after `cursor` (`undefined` for the first page).
79
+ */
80
+ async function* paginate<T>(
81
+ fetchPage: (cursor: string | undefined) => Promise<{ data: T[]; nextCursor: string | null }>
82
+ ): AsyncGenerator<T[]> {
83
+ let cursor: string | undefined
84
+ do {
85
+ const page = await fetchPage(cursor)
86
+ yield page.data
87
+ cursor = page.nextCursor ?? undefined
88
+ } while (cursor)
89
+ }
90
+
91
+ /** Get a provider by ID, or `null` when it does not exist. */
92
+ export function getProvider({ client, network }: IndexerApi, providerId: bigint): Promise<Provider | null> {
93
+ return orNull(
94
+ client
95
+ .GET('/{network}/providers/{providerId}', {
96
+ params: { path: { network, providerId: providerId.toString() } },
97
+ })
98
+ .then((result) => unwrap(result).data)
99
+ )
100
+ }
101
+
102
+ /** List every provider matching the filters. */
103
+ export function listProviders(
104
+ { client, network }: IndexerApi,
105
+ query: { active?: boolean; approved?: boolean; endorsed?: boolean } = {}
106
+ ): Promise<Provider[]> {
107
+ return collect(
108
+ paginate((cursor) =>
109
+ client
110
+ .GET('/{network}/providers', {
111
+ params: {
112
+ path: { network },
113
+ query: {
114
+ limit: MAX_PAGE_SIZE,
115
+ cursor,
116
+ active: bool(query.active),
117
+ approved: bool(query.approved),
118
+ endorsed: bool(query.endorsed),
119
+ },
120
+ },
121
+ })
122
+ .then(unwrap)
123
+ )
124
+ )
125
+ }
126
+
127
+ /** Get a data set by ID, or `null` when it does not exist. */
128
+ export function getDataSet({ client, network }: IndexerApi, dataSetId: bigint): Promise<DataSet | null> {
129
+ return orNull(
130
+ client
131
+ .GET('/{network}/data-sets/{dataSetId}', {
132
+ params: { path: { network, dataSetId: dataSetId.toString() } },
133
+ })
134
+ .then((result) => unwrap(result).data)
135
+ )
136
+ }
137
+
138
+ /** List every data set matching the filters, newest ID first. */
139
+ export function listDataSets(
140
+ { client, network }: IndexerApi,
141
+ query: { owner?: string; providerId?: bigint; deleted?: boolean; withCdn?: boolean }
142
+ ): Promise<DataSet[]> {
143
+ return collect(
144
+ paginate((cursor) =>
145
+ client
146
+ .GET('/{network}/data-sets', {
147
+ params: {
148
+ path: { network },
149
+ query: {
150
+ limit: MAX_PAGE_SIZE,
151
+ cursor,
152
+ owner: query.owner,
153
+ provider_id: query.providerId?.toString(),
154
+ deleted: bool(query.deleted),
155
+ with_cdn: bool(query.withCdn),
156
+ },
157
+ },
158
+ })
159
+ .then(unwrap)
160
+ )
161
+ )
162
+ }
163
+
164
+ /** Walk the pieces of one data set page by page, newest piece ID first. */
165
+ export function dataSetPiecePages(
166
+ { client, network }: IndexerApi,
167
+ dataSetId: bigint,
168
+ query: { removed?: boolean } = {}
169
+ ): AsyncGenerator<Piece[]> {
170
+ return paginate((cursor) =>
171
+ client
172
+ .GET('/{network}/data-sets/{dataSetId}/pieces', {
173
+ params: {
174
+ path: { network, dataSetId: dataSetId.toString() },
175
+ query: { limit: MAX_PAGE_SIZE, cursor, removed: bool(query.removed) },
176
+ },
177
+ })
178
+ .then(unwrap)
179
+ )
180
+ }
181
+
182
+ /** List every piece in one data set, newest piece ID first. */
183
+ export function listDataSetPieces(
184
+ api: IndexerApi,
185
+ dataSetId: bigint,
186
+ query: { removed?: boolean } = {}
187
+ ): Promise<Piece[]> {
188
+ return collect(dataSetPiecePages(api, dataSetId, query))
189
+ }
190
+
191
+ /**
192
+ * Walk the pieces of every data set a provider stores, page by page, newest data set and piece
193
+ * first. One walk covers all of the provider's data sets.
194
+ */
195
+ export function providerPiecePages(
196
+ { client, network }: IndexerApi,
197
+ providerId: bigint,
198
+ query: { removed?: boolean } = {}
199
+ ): AsyncGenerator<PieceWithDataSet[]> {
200
+ return paginate((cursor) =>
201
+ client
202
+ .GET('/{network}/pieces', {
203
+ params: {
204
+ path: { network },
205
+ query: { limit: MAX_PAGE_SIZE, cursor, provider_id: providerId.toString(), removed: bool(query.removed) },
206
+ },
207
+ })
208
+ .then(unwrap)
209
+ )
210
+ }
211
+
212
+ /** Encode an optional boolean filter as a query string value. */
213
+ function bool(value: boolean | undefined): 'true' | 'false' | undefined {
214
+ return value === undefined ? undefined : value ? 'true' : 'false'
215
+ }
216
+
217
+ /** Convert API piece metadata to the string record stored on local operations. */
218
+ export function toStringMetadata(metadata: Record<string, unknown> | null): Record<string, string> {
219
+ return Object.fromEntries(Object.entries(metadata ?? {}).map(([key, value]) => [key, String(value)]))
220
+ }
@@ -1,9 +1,9 @@
1
1
  import * as p from '@clack/prompts'
2
2
  import * as SP from '@filoz/synapse-core/sp'
3
3
  import { getPdpDataSet } from '@filoz/synapse-core/warm-storage'
4
- import { and, asc, eq } from 'drizzle-orm'
5
4
  import { Cli, z } from 'incur'
6
5
  import { isAddress } from 'viem'
6
+ import { getDataSet, getProvider, listDataSetPieces, listDataSets, listProviders } from '../api.ts'
7
7
  import { contextMiddleware, contextSchema } from '../middleware.ts'
8
8
  import { globalOptions, hashLink } from '../utils.ts'
9
9
 
@@ -18,42 +18,33 @@ datasets.command('list', {
18
18
  options: globalOptions.extend({
19
19
  providerId: z.coerce.bigint().optional().describe('Filter datasets by provider ID'),
20
20
  payer: z.string().refine(isAddress, 'Invalid address').optional().describe('Filter datasets by payer address'),
21
+ stats: z.boolean().optional().default(false).describe('Include piece counts (slow: walks every piece)'),
21
22
  }),
22
23
  middleware: [contextMiddleware],
23
24
  run: async (c) => {
24
25
  try {
25
- const schema = c.var.indexerDb._.fullSchema
26
+ const { api } = c.var
26
27
  const payer = c.options.payer?.toLowerCase() ?? c.var.client.account.address.toLowerCase()
27
- const conditions = [eq(schema.dataSets.deleted, false), eq(schema.dataSets.payer, payer)]
28
+ const [rows, providers] = await Promise.all([
29
+ listDataSets(api, { owner: payer, providerId: c.options.providerId, deleted: false }),
30
+ listProviders(api),
31
+ ])
32
+ const serviceUrls = new Map(providers.map((provider) => [provider.providerId, provider.serviceUrl]))
28
33
 
29
- if (c.options.providerId != null) {
30
- conditions.push(eq(schema.dataSets.providerId, c.options.providerId))
31
- }
32
-
33
- const datasets = await c.var.indexerDb.query.dataSets.findMany({
34
- where: and(...conditions),
35
- with: {
36
- provider: true,
37
- pieces: {
38
- where: eq(c.var.indexerDb._.fullSchema.pieces.removed, false),
39
- },
40
- },
41
- orderBy: [asc(schema.dataSets.dataSetId)],
42
- })
43
-
44
- const datasetsFlattened = datasets.map((dataset) => {
45
- const { provider, pieces } = dataset
46
- return {
34
+ const datasetsFlattened = []
35
+ for (const dataset of rows.reverse()) {
36
+ datasetsFlattened.push({
47
37
  id: dataset.dataSetId,
48
38
  withCdn: dataset.withCdn,
49
39
  withIpfsIndexing: dataset.withIpfsIndexing,
50
40
  source: dataset.source,
51
- provider: provider.serviceUrl,
41
+ provider: dataset.providerId == null ? null : serviceUrls.get(dataset.providerId),
52
42
  pdpEndEpoch: dataset.pdpEndEpoch,
53
- pieces: pieces.length,
54
- // metadata: JSON.stringify(dataset.metadata),
55
- }
56
- })
43
+ ...(c.options.stats && {
44
+ pieces: (await listDataSetPieces(api, BigInt(dataset.dataSetId), { removed: false })).length,
45
+ }),
46
+ })
47
+ }
57
48
 
58
49
  return c.ok({
59
50
  datasets: datasetsFlattened,
@@ -92,12 +83,19 @@ datasets.command('terminate', {
92
83
  retryable: false,
93
84
  })
94
85
  }
86
+ if (!dataset.provider) {
87
+ return c.error({
88
+ code: 'PROVIDER_NOT_FOUND',
89
+ message: `Provider ${dataset.providerId} for dataset ${dataset.dataSetId} is not available`,
90
+ retryable: false,
91
+ })
92
+ }
95
93
  if (isInteractive) {
96
- spinner.message(`Terminating dataset ${dataset?.dataSetId} at ${dataset?.provider.pdp.serviceURL}...`)
94
+ spinner.message(`Terminating dataset ${dataset.dataSetId} at ${dataset.provider.pdp.serviceURL}...`)
97
95
  }
98
96
  const result = await SP.terminateService(c.var.client, {
99
97
  dataSetId: c.args.id,
100
- serviceURL: dataset?.provider.pdp.serviceURL,
98
+ serviceURL: dataset.provider.pdp.serviceURL,
101
99
  })
102
100
 
103
101
  const waitForResult = await SP.waitForTerminateService({
@@ -138,15 +136,8 @@ datasets.command('show', {
138
136
  middleware: [contextMiddleware],
139
137
  run: async (c) => {
140
138
  try {
141
- const dataset = await c.var.indexerDb.query.dataSets.findFirst({
142
- where: eq(c.var.indexerDb._.fullSchema.dataSets.dataSetId, c.args.id),
143
- with: {
144
- provider: true,
145
- pieces: {
146
- where: eq(c.var.indexerDb._.fullSchema.pieces.removed, false),
147
- },
148
- },
149
- })
139
+ const { api } = c.var
140
+ const dataset = await getDataSet(api, c.args.id)
150
141
 
151
142
  if (!dataset) {
152
143
  return c.error({
@@ -155,7 +146,11 @@ datasets.command('show', {
155
146
  retryable: false,
156
147
  })
157
148
  }
158
- return c.ok(dataset)
149
+ const [provider, pieces] = await Promise.all([
150
+ dataset.providerId == null ? null : getProvider(api, BigInt(dataset.providerId)),
151
+ listDataSetPieces(api, c.args.id, { removed: false }),
152
+ ])
153
+ return c.ok({ ...dataset, provider, pieces: pieces.reverse() })
159
154
  } catch (error) {
160
155
  if (c.options.debug) {
161
156
  console.error(error)
@@ -1,6 +1,7 @@
1
- import { and, asc, count, eq, inArray, isNull, lte, or, type SQLWrapper, sum } from 'drizzle-orm'
2
1
  import { Cli, z } from 'incur'
3
2
  import { getBlockNumber } from 'viem/actions'
3
+ import { listProviders, providerPiecePages } from '../api.ts'
4
+ import { getActiveDataSetIds } from '../db/get-active-data-set-ids.ts'
4
5
  import { contextMiddleware, contextSchema } from '../middleware.ts'
5
6
  import { globalOptions } from '../utils.ts'
6
7
 
@@ -12,6 +13,13 @@ function formatBytesAsGb(bytes: bigint): string {
12
13
  return `${whole}.${fraction.toString().padStart(2, '0')} GB`
13
14
  }
14
15
 
16
+ /** Compare decimal ID strings numerically. */
17
+ function compareIds(a: string, b: string): number {
18
+ const x = BigInt(a)
19
+ const y = BigInt(b)
20
+ return x < y ? -1 : x > y ? 1 : 0
21
+ }
22
+
15
23
  export const providers = Cli.create('providers', {
16
24
  description: 'Provider commands',
17
25
  options: globalOptions,
@@ -22,51 +30,36 @@ providers.command('list', {
22
30
  description: 'List all providers from the indexer',
23
31
  options: globalOptions.extend({
24
32
  all: z.boolean().optional().default(false).describe('Include all providers'),
33
+ stats: z
34
+ .boolean()
35
+ .optional()
36
+ .default(false)
37
+ .describe('Include piece count and total size (slow: walks every piece of each provider)'),
25
38
  }),
26
39
  middleware: [contextMiddleware],
27
40
  run: async (c) => {
28
41
  try {
29
- const schema = c.var.indexerDb._.fullSchema
30
- const filters: (SQLWrapper | undefined)[] = [
31
- eq(schema.providers.providerActive, true),
32
- eq(schema.providers.pdpProductActive, true),
33
- ]
34
- if (!c.options.all) {
35
- filters.push(or(eq(schema.providers.approved, true), eq(schema.providers.endorsed, true)))
36
- }
37
- const blockNumber = await getBlockNumber(c.var.client)
38
- const rows = await c.var.indexerDb.query.providers.findMany({
39
- orderBy: [asc(schema.providers.providerId)],
40
- where: and(...filters),
41
- })
42
-
43
- const providerIds = rows.map((provider) => provider.providerId)
44
- const statsByProviderId = new Map<bigint, { pieceCount: number; totalSize: bigint }>()
42
+ const { api } = c.var
43
+ const rows = (await listProviders(api, { active: true }))
44
+ .filter((provider) => provider.pdpProductActive && (c.options.all || provider.approved || provider.endorsed))
45
+ .sort((a, b) => compareIds(a.providerId, b.providerId))
45
46
 
46
- if (providerIds.length > 0) {
47
- const stats = await c.var.indexerDb
48
- .select({
49
- providerId: schema.dataSets.providerId,
50
- pieceCount: count(schema.pieces.pieceId),
51
- totalSize: sum(schema.pieces.rawSize),
52
- })
53
- .from(schema.pieces)
54
- .innerJoin(schema.dataSets, eq(schema.pieces.dataSetId, schema.dataSets.dataSetId))
55
- .where(
56
- and(
57
- inArray(schema.dataSets.providerId, providerIds),
58
- eq(schema.dataSets.deleted, false),
59
- or(isNull(schema.dataSets.pdpEndEpoch), lte(schema.dataSets.pdpEndEpoch, blockNumber)),
60
- eq(schema.pieces.removed, false)
61
- )
62
- )
63
- .groupBy(schema.dataSets.providerId)
47
+ const statsByProviderId = new Map<string, { pieceCount: number; totalSize: bigint }>()
64
48
 
65
- for (const stat of stats) {
66
- statsByProviderId.set(stat.providerId, {
67
- pieceCount: stat.pieceCount,
68
- totalSize: stat.totalSize == null ? 0n : BigInt(stat.totalSize),
69
- })
49
+ if (c.options.stats && rows.length > 0) {
50
+ const blockNumber = await getBlockNumber(c.var.client)
51
+ for (const provider of rows) {
52
+ const providerId = BigInt(provider.providerId)
53
+ const activeDataSetIds = await getActiveDataSetIds({ api, providerId, blockNumber })
54
+ const stats = { pieceCount: 0, totalSize: 0n }
55
+ for await (const pieces of providerPiecePages(api, providerId, { removed: false })) {
56
+ for (const piece of pieces) {
57
+ if (!activeDataSetIds.has(piece.dataSetId)) continue
58
+ stats.pieceCount++
59
+ stats.totalSize += BigInt(piece.rawSize ?? 0)
60
+ }
61
+ }
62
+ statsByProviderId.set(provider.providerId, stats)
70
63
  }
71
64
  }
72
65
 
@@ -78,8 +71,7 @@ providers.command('list', {
78
71
  serviceUrl: provider.serviceUrl,
79
72
  approved: provider.approved,
80
73
  endorsed: provider.endorsed,
81
- pieceCount: stats?.pieceCount ?? 0,
82
- totalSize: formatBytesAsGb(stats?.totalSize ?? 0n),
74
+ ...(stats && { pieceCount: stats.pieceCount, totalSize: formatBytesAsGb(stats.totalSize) }),
83
75
  }
84
76
  })
85
77
 
@@ -135,7 +135,11 @@ repair.command('run', {
135
135
  }),
136
136
  options: globalOptions.extend({
137
137
  concurrency: z.coerce.number().min(1).max(10).default(4).describe('Concurrency level'),
138
- batchSize: z.coerce.number().min(1).max(40).default(40).describe('Max pieces per batch'),
138
+ batchSize: z.coerce
139
+ .number()
140
+ .min(1)
141
+ .optional()
142
+ .describe('Max pieces per batch. Defaults to as many as fit in one addPieces message'),
139
143
  payer: z.string().refine(isAddress, 'Invalid address').optional().describe('Payer address'),
140
144
  }),
141
145
  middleware: [contextMiddleware],
@@ -136,7 +136,11 @@ replicate.command('run', {
136
136
  }),
137
137
  options: globalOptions.extend({
138
138
  concurrency: z.coerce.number().min(1).max(10).default(4).describe('Concurrency level'),
139
- batchSize: z.coerce.number().min(1).max(40).default(40).describe('Max pieces per batch'),
139
+ batchSize: z.coerce
140
+ .number()
141
+ .min(1)
142
+ .optional()
143
+ .describe('Max pieces per batch. Defaults to as many as fit in one addPieces message'),
140
144
  payer: z.string().refine(isAddress, 'Invalid address').optional().describe('Payer address'),
141
145
  }),
142
146
  middleware: [contextMiddleware],
@@ -137,7 +137,11 @@ secondCopy.command('run', {
137
137
  }),
138
138
  options: globalOptions.extend({
139
139
  concurrency: z.coerce.number().min(1).max(10).default(4).describe('Concurrency level'),
140
- batchSize: z.coerce.number().min(1).max(40).default(40).describe('Max pieces per batch'),
140
+ batchSize: z.coerce
141
+ .number()
142
+ .min(1)
143
+ .optional()
144
+ .describe('Max pieces per batch. Defaults to as many as fit in one addPieces message'),
141
145
  payer: z.string().refine(isAddress, 'Invalid address').optional().describe('Payer address'),
142
146
  }),
143
147
  middleware: [contextMiddleware],