@filoz/repair-cli 0.4.0 → 1.0.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 (106) hide show
  1. package/dist/package.json +3 -3
  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 +3 -5
  7. package/dist/src/commands/datasets.d.ts.map +1 -1
  8. package/dist/src/commands/datasets.js +24 -33
  9. package/dist/src/commands/datasets.js.map +1 -1
  10. package/dist/src/commands/providers.d.ts +3 -5
  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 +3 -5
  15. package/dist/src/commands/repair.d.ts.map +1 -1
  16. package/dist/src/commands/replicate.d.ts +3 -5
  17. package/dist/src/commands/replicate.d.ts.map +1 -1
  18. package/dist/src/commands/second-copy.d.ts +3 -5
  19. package/dist/src/commands/second-copy.d.ts.map +1 -1
  20. package/dist/src/commands/session-key.d.ts +3 -5
  21. package/dist/src/commands/session-key.d.ts.map +1 -1
  22. package/dist/src/commands/setup.d.ts.map +1 -1
  23. package/dist/src/commands/setup.js +11 -32
  24. package/dist/src/commands/setup.js.map +1 -1
  25. package/dist/src/commands/wallet.d.ts +3 -5
  26. package/dist/src/commands/wallet.d.ts.map +1 -1
  27. package/dist/src/db/dedupe-cids.d.ts +6 -7
  28. package/dist/src/db/dedupe-cids.d.ts.map +1 -1
  29. package/dist/src/db/dedupe-cids.js +2 -14
  30. package/dist/src/db/dedupe-cids.js.map +1 -1
  31. package/dist/src/db/find-repair-dataset.d.ts +3 -3
  32. package/dist/src/db/find-repair-dataset.d.ts.map +1 -1
  33. package/dist/src/db/find-repair-dataset.js +16 -14
  34. package/dist/src/db/find-repair-dataset.js.map +1 -1
  35. package/dist/src/db/get-active-data-set-ids.d.ts +9 -0
  36. package/dist/src/db/get-active-data-set-ids.d.ts.map +1 -0
  37. package/dist/src/db/get-active-data-set-ids.js +9 -0
  38. package/dist/src/db/get-active-data-set-ids.js.map +1 -0
  39. package/dist/src/db/get-alternate-provider-urls.d.ts +6 -0
  40. package/dist/src/db/get-alternate-provider-urls.d.ts.map +1 -0
  41. package/dist/src/db/get-alternate-provider-urls.js +24 -0
  42. package/dist/src/db/get-alternate-provider-urls.js.map +1 -0
  43. package/dist/src/db/get-pieces.d.ts +8 -27
  44. package/dist/src/db/get-pieces.d.ts.map +1 -1
  45. package/dist/src/db/get-pieces.js +44 -97
  46. package/dist/src/db/get-pieces.js.map +1 -1
  47. package/dist/src/db/get-repair-provider.d.ts +2 -2
  48. package/dist/src/db/get-repair-provider.d.ts.map +1 -1
  49. package/dist/src/db/get-repair-provider.js +8 -20
  50. package/dist/src/db/get-repair-provider.js.map +1 -1
  51. package/dist/src/db/repair-create.js +3 -3
  52. package/dist/src/db/repair-create.js.map +1 -1
  53. package/dist/src/db/replicate-create.d.ts.map +1 -1
  54. package/dist/src/db/replicate-create.js +13 -21
  55. package/dist/src/db/replicate-create.js.map +1 -1
  56. package/dist/src/db/second-copy-create.js +2 -2
  57. package/dist/src/db/second-copy-create.js.map +1 -1
  58. package/dist/src/local-schema.d.ts +1 -1
  59. package/dist/src/middleware.d.ts +7 -11
  60. package/dist/src/middleware.d.ts.map +1 -1
  61. package/dist/src/middleware.js +6 -10
  62. package/dist/src/middleware.js.map +1 -1
  63. package/dist/src/pipeline/add-pieces.d.ts +3 -3
  64. package/dist/src/pipeline/add-pieces.d.ts.map +1 -1
  65. package/dist/src/pipeline/add-pieces.js +15 -7
  66. package/dist/src/pipeline/add-pieces.js.map +1 -1
  67. package/dist/src/pipeline/create-datasets.d.ts +4 -4
  68. package/dist/src/pipeline/create-datasets.d.ts.map +1 -1
  69. package/dist/src/pipeline/create-datasets.js +7 -14
  70. package/dist/src/pipeline/create-datasets.js.map +1 -1
  71. package/dist/src/types.d.ts +6 -7
  72. package/dist/src/types.d.ts.map +1 -1
  73. package/dist/src/utils.d.ts +7 -10
  74. package/dist/src/utils.d.ts.map +1 -1
  75. package/dist/src/utils.js +1 -2
  76. package/dist/src/utils.js.map +1 -1
  77. package/package.json +3 -3
  78. package/readme.md +16 -13
  79. package/src/api.ts +220 -0
  80. package/src/commands/datasets.ts +24 -36
  81. package/src/commands/providers.ts +34 -42
  82. package/src/commands/setup.ts +12 -35
  83. package/src/db/dedupe-cids.ts +7 -24
  84. package/src/db/find-repair-dataset.ts +17 -28
  85. package/src/db/get-active-data-set-ids.ts +29 -0
  86. package/src/db/get-alternate-provider-urls.ts +43 -0
  87. package/src/db/get-pieces.ts +82 -214
  88. package/src/db/get-repair-provider.ts +10 -30
  89. package/src/db/repair-create.ts +3 -3
  90. package/src/db/replicate-create.ts +13 -21
  91. package/src/db/second-copy-create.ts +2 -2
  92. package/src/middleware.ts +7 -11
  93. package/src/pipeline/add-pieces.ts +20 -10
  94. package/src/pipeline/create-datasets.ts +9 -29
  95. package/src/types.ts +9 -7
  96. package/src/utils.ts +2 -4
  97. package/dist/src/db/find-providers-by-cid.d.ts +0 -9
  98. package/dist/src/db/find-providers-by-cid.d.ts.map +0 -1
  99. package/dist/src/db/find-providers-by-cid.js +0 -43
  100. package/dist/src/db/find-providers-by-cid.js.map +0 -1
  101. package/dist/src/indexer-schema.d.ts +0 -549
  102. package/dist/src/indexer-schema.d.ts.map +0 -1
  103. package/dist/src/indexer-schema.js +0 -56
  104. package/dist/src/indexer-schema.js.map +0 -1
  105. package/src/db/find-providers-by-cid.ts +0 -70
  106. package/src/indexer-schema.ts +0 -77
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,
@@ -138,15 +129,8 @@ datasets.command('show', {
138
129
  middleware: [contextMiddleware],
139
130
  run: async (c) => {
140
131
  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
- })
132
+ const { api } = c.var
133
+ const dataset = await getDataSet(api, c.args.id)
150
134
 
151
135
  if (!dataset) {
152
136
  return c.error({
@@ -155,7 +139,11 @@ datasets.command('show', {
155
139
  retryable: false,
156
140
  })
157
141
  }
158
- return c.ok(dataset)
142
+ const [provider, pieces] = await Promise.all([
143
+ dataset.providerId == null ? null : getProvider(api, BigInt(dataset.providerId)),
144
+ listDataSetPieces(api, c.args.id, { removed: false }),
145
+ ])
146
+ return c.ok({ ...dataset, provider, pieces: pieces.reverse() })
159
147
  } catch (error) {
160
148
  if (c.options.debug) {
161
149
  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
 
@@ -3,21 +3,16 @@ import { Cli, z } from 'incur'
3
3
  import path from 'path'
4
4
  import type { Hash } from 'viem'
5
5
  import { privateKeyToAccount } from 'viem/accounts'
6
+ import { DEFAULT_API_URL } from '../api.ts'
6
7
  import { config, createLocalDatabase, globalOptions, migrateLocalDatabase } from '../utils.ts'
7
8
 
8
- function validatePostgresUrl(value: string) {
9
- let url: URL
9
+ function validateHttpUrl(value: string) {
10
10
  try {
11
- url = new URL(value)
11
+ const url = new URL(value)
12
+ return url.protocol === 'http:' || url.protocol === 'https:'
12
13
  } catch {
13
14
  return false
14
15
  }
15
-
16
- if (url.protocol !== 'postgresql:' && url.protocol !== 'postgres:') {
17
- return false
18
- }
19
-
20
- return Boolean(url.hostname && url.username && url.password && url.pathname.length > 1)
21
16
  }
22
17
 
23
18
  export const setup = Cli.create('setup', {
@@ -44,35 +39,18 @@ export const setup = Cli.create('setup', {
44
39
  })
45
40
  }
46
41
 
47
- // Indexer URL
48
- const indexerMainnetUrl = await p.text({
49
- message: 'Enter your Mainnet Indexer Postgres URL',
50
- validate(value) {
51
- if (!value || !validatePostgresUrl(value)) {
52
- return `Invalid postgres URL!`
53
- }
54
- },
55
- initialValue: config.get('indexerMainnetUrl'),
56
- withGuide: false,
57
- })
58
- if (p.isCancel(indexerMainnetUrl)) {
59
- return c.error({
60
- code: 'SETUP_CANCELLED',
61
- message: 'Setup cancelled',
62
- retryable: false,
63
- })
64
- }
65
- const indexerCalibrationUrl = await p.text({
66
- message: 'Enter your Calibration Indexer Postgres URL',
42
+ // API URL
43
+ const apiUrl = await p.text({
44
+ message: 'Enter the Filecoin API URL',
67
45
  validate(value) {
68
- if (!value || !validatePostgresUrl(value)) {
69
- return `Invalid postgres URL!`
46
+ if (!value || !validateHttpUrl(value)) {
47
+ return `Invalid http(s) URL!`
70
48
  }
71
49
  },
72
- initialValue: config.get('indexerCalibrationUrl'),
50
+ initialValue: config.get('apiUrl') ?? DEFAULT_API_URL,
73
51
  withGuide: false,
74
52
  })
75
- if (p.isCancel(indexerCalibrationUrl)) {
53
+ if (p.isCancel(apiUrl)) {
76
54
  return c.error({
77
55
  code: 'SETUP_CANCELLED',
78
56
  message: 'Setup cancelled',
@@ -129,8 +107,7 @@ export const setup = Cli.create('setup', {
129
107
 
130
108
  // Set config
131
109
  config.set('privateKey', pk)
132
- config.set('indexerMainnetUrl', indexerMainnetUrl)
133
- config.set('indexerCalibrationUrl', indexerCalibrationUrl)
110
+ config.set('apiUrl', apiUrl)
134
111
  config.set('chainId', chainId)
135
112
  config.set('dbPath', dbPath)
136
113
  config.set('source', source)
@@ -1,37 +1,20 @@
1
- import { and, eq, inArray } from 'drizzle-orm'
2
1
  import type { OperationSelect } from '../local-schema.ts'
3
- import type { IndexerDatabase, LocalDatabase } from '../types.ts'
2
+ import type { LocalDatabase } from '../types.ts'
4
3
  import { upsertOperations } from './upsert-operations.ts'
5
4
 
6
- export type SyncPiecesOnchainOptions = {
7
- indexerDb: IndexerDatabase
5
+ export type DedupeCidsOptions = {
8
6
  localDb: LocalDatabase
9
- dataSetId: bigint
7
+ /** CIDs already in the target repair dataset. */
8
+ existingCids: ReadonlySet<string>
10
9
  operations: OperationSelect[]
11
10
  }
12
11
 
13
12
  /**
14
- * Dedupe operations by CID on the target repair dataset.
13
+ * Mark operations whose CID is already in the target repair dataset as completed.
15
14
  * Returns operations that are not onchain.
16
15
  */
17
- export async function dedupeCids({ indexerDb, localDb, dataSetId, operations }: SyncPiecesOnchainOptions) {
18
- const cids = operations.map((operation) => operation.cid)
19
- const schema = indexerDb._.fullSchema
20
-
21
- const rows = await indexerDb
22
- .select({ cid: schema.pieces.cid })
23
- .from(schema.pieces)
24
- .where(
25
- and(eq(schema.pieces.dataSetId, dataSetId), eq(schema.pieces.removed, false), inArray(schema.pieces.cid, cids))
26
- )
27
-
28
- const existingCids = new Set<string>()
29
- const completedOperations: OperationSelect[] = []
30
-
31
- for (const row of rows) {
32
- completedOperations.push(...operations.filter((operation) => operation.cid === row.cid))
33
- existingCids.add(row.cid)
34
- }
16
+ export async function dedupeCids({ localDb, existingCids, operations }: DedupeCidsOptions) {
17
+ const completedOperations = operations.filter((operation) => existingCids.has(operation.cid))
35
18
 
36
19
  if (completedOperations.length > 0) {
37
20
  await upsertOperations({
@@ -1,49 +1,38 @@
1
- import { and, asc, eq, isNull } from 'drizzle-orm'
2
1
  import type { Address } from 'viem'
3
- import type { IndexerDatabase } from '../types.ts'
2
+ import { listDataSets } from '../api.ts'
3
+ import type { IndexerApi } from '../types.ts'
4
4
 
5
5
  export type FindRepairDatasetOptions = {
6
- indexerDb: IndexerDatabase
6
+ api: IndexerApi
7
7
  providerId: bigint
8
8
  payer: Address
9
9
  source: string
10
10
  }
11
11
 
12
12
  /**
13
- * Find one IPFS-enabled repair dataset for a provider at the repair block, if it exists.
13
+ * Find one live IPFS-enabled repair dataset for a provider, if it exists.
14
14
  *
15
15
  * When multiple datasets match, the lowest `dataSetId` is returned.
16
16
  */
17
17
  export async function findRepairDataset({
18
- indexerDb,
18
+ api,
19
19
  providerId,
20
20
  payer,
21
21
  source,
22
22
  }: FindRepairDatasetOptions): Promise<bigint | null> {
23
- const schema = indexerDb._.fullSchema
23
+ const dataSets = await listDataSets(api, {
24
+ owner: payer.toLowerCase(),
25
+ providerId,
26
+ deleted: false,
27
+ withCdn: false,
28
+ })
24
29
 
25
- const result = await indexerDb
26
- .select({
27
- dataSetId: schema.dataSets.dataSetId,
28
- })
29
- .from(schema.dataSets)
30
- .where(
31
- and(
32
- eq(schema.dataSets.providerId, providerId),
33
- eq(schema.dataSets.deleted, false),
34
- isNull(schema.dataSets.pdpEndEpoch),
35
- eq(schema.dataSets.payer, payer.toLowerCase()),
36
- eq(schema.dataSets.source, source),
37
- eq(schema.dataSets.withCdn, false),
38
- eq(schema.dataSets.withIpfsIndexing, true)
39
- )
40
- )
41
- .orderBy(asc(schema.dataSets.dataSetId))
42
- .limit(1)
43
-
44
- if (result.length === 0) {
45
- return null
30
+ let dataSetId: bigint | null = null
31
+ for (const dataSet of dataSets) {
32
+ if (dataSet.pdpEndEpoch != null || dataSet.source !== source || !dataSet.withIpfsIndexing) continue
33
+ const id = BigInt(dataSet.dataSetId)
34
+ if (dataSetId == null || id < dataSetId) dataSetId = id
46
35
  }
47
36
 
48
- return result[0].dataSetId
37
+ return dataSetId
49
38
  }
@@ -0,0 +1,29 @@
1
+ import { type DataSet, listDataSets } from '../api.ts'
2
+ import type { IndexerApi } from '../types.ts'
3
+
4
+ /**
5
+ * Whether a data set's pieces count at the repair block: not deleted, and either not terminated
6
+ * (no PDP end epoch) or terminated with an end epoch after `blockNumber`, so the provider still
7
+ * has to prove it.
8
+ */
9
+ export function isDataSetActiveAt(dataSet: Pick<DataSet, 'deleted' | 'pdpEndEpoch'>, blockNumber: bigint): boolean {
10
+ return !dataSet.deleted && (dataSet.pdpEndEpoch == null || BigInt(dataSet.pdpEndEpoch) > blockNumber)
11
+ }
12
+
13
+ /**
14
+ * IDs of a provider's data sets that are active at the repair block (see {@link isDataSetActiveAt}).
15
+ */
16
+ export async function getActiveDataSetIds({
17
+ api,
18
+ providerId,
19
+ blockNumber,
20
+ }: {
21
+ api: IndexerApi
22
+ providerId: bigint
23
+ blockNumber: bigint
24
+ }): Promise<Set<string>> {
25
+ const dataSets = await listDataSets(api, { providerId, deleted: false })
26
+ return new Set(
27
+ dataSets.filter((dataSet) => isDataSetActiveAt(dataSet, blockNumber)).map((dataSet) => dataSet.dataSetId)
28
+ )
29
+ }