@filoz/repair-cli 0.3.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 (125) 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/cli.js +2 -0
  7. package/dist/src/cli.js.map +1 -1
  8. package/dist/src/commands/datasets.d.ts +3 -5
  9. package/dist/src/commands/datasets.d.ts.map +1 -1
  10. package/dist/src/commands/datasets.js +24 -33
  11. package/dist/src/commands/datasets.js.map +1 -1
  12. package/dist/src/commands/providers.d.ts +3 -5
  13. package/dist/src/commands/providers.d.ts.map +1 -1
  14. package/dist/src/commands/providers.js +32 -33
  15. package/dist/src/commands/providers.js.map +1 -1
  16. package/dist/src/commands/repair.d.ts +3 -5
  17. package/dist/src/commands/repair.d.ts.map +1 -1
  18. package/dist/src/commands/repair.js +3 -3
  19. package/dist/src/commands/repair.js.map +1 -1
  20. package/dist/src/commands/replicate.d.ts +3 -5
  21. package/dist/src/commands/replicate.d.ts.map +1 -1
  22. package/dist/src/commands/replicate.js +3 -3
  23. package/dist/src/commands/replicate.js.map +1 -1
  24. package/dist/src/commands/second-copy.d.ts +23 -0
  25. package/dist/src/commands/second-copy.d.ts.map +1 -0
  26. package/dist/src/commands/second-copy.js +175 -0
  27. package/dist/src/commands/second-copy.js.map +1 -0
  28. package/dist/src/commands/session-key.d.ts +3 -5
  29. package/dist/src/commands/session-key.d.ts.map +1 -1
  30. package/dist/src/commands/setup.d.ts.map +1 -1
  31. package/dist/src/commands/setup.js +11 -32
  32. package/dist/src/commands/setup.js.map +1 -1
  33. package/dist/src/commands/wallet.d.ts +3 -5
  34. package/dist/src/commands/wallet.d.ts.map +1 -1
  35. package/dist/src/db/dedupe-cids.d.ts +6 -7
  36. package/dist/src/db/dedupe-cids.d.ts.map +1 -1
  37. package/dist/src/db/dedupe-cids.js +2 -14
  38. package/dist/src/db/dedupe-cids.js.map +1 -1
  39. package/dist/src/db/find-repair-dataset.d.ts +3 -3
  40. package/dist/src/db/find-repair-dataset.d.ts.map +1 -1
  41. package/dist/src/db/find-repair-dataset.js +16 -14
  42. package/dist/src/db/find-repair-dataset.js.map +1 -1
  43. package/dist/src/db/get-active-data-set-ids.d.ts +9 -0
  44. package/dist/src/db/get-active-data-set-ids.d.ts.map +1 -0
  45. package/dist/src/db/get-active-data-set-ids.js +9 -0
  46. package/dist/src/db/get-active-data-set-ids.js.map +1 -0
  47. package/dist/src/db/get-alternate-provider-urls.d.ts +6 -0
  48. package/dist/src/db/get-alternate-provider-urls.d.ts.map +1 -0
  49. package/dist/src/db/get-alternate-provider-urls.js +24 -0
  50. package/dist/src/db/get-alternate-provider-urls.js.map +1 -0
  51. package/dist/src/db/get-pieces.d.ts +8 -27
  52. package/dist/src/db/get-pieces.d.ts.map +1 -1
  53. package/dist/src/db/get-pieces.js +44 -97
  54. package/dist/src/db/get-pieces.js.map +1 -1
  55. package/dist/src/db/get-repair-provider.d.ts +2 -2
  56. package/dist/src/db/get-repair-provider.d.ts.map +1 -1
  57. package/dist/src/db/get-repair-provider.js +8 -20
  58. package/dist/src/db/get-repair-provider.js.map +1 -1
  59. package/dist/src/db/repair-create.d.ts.map +1 -1
  60. package/dist/src/db/repair-create.js +5 -3
  61. package/dist/src/db/repair-create.js.map +1 -1
  62. package/dist/src/db/replicate-create.d.ts.map +1 -1
  63. package/dist/src/db/replicate-create.js +15 -21
  64. package/dist/src/db/replicate-create.js.map +1 -1
  65. package/dist/src/db/second-copy-create.d.ts +10 -0
  66. package/dist/src/db/second-copy-create.d.ts.map +1 -0
  67. package/dist/src/db/second-copy-create.js +104 -0
  68. package/dist/src/db/second-copy-create.js.map +1 -0
  69. package/dist/src/local-schema.d.ts +41 -1
  70. package/dist/src/local-schema.d.ts.map +1 -1
  71. package/dist/src/local-schema.js +2 -0
  72. package/dist/src/local-schema.js.map +1 -1
  73. package/dist/src/middleware.d.ts +7 -11
  74. package/dist/src/middleware.d.ts.map +1 -1
  75. package/dist/src/middleware.js +6 -10
  76. package/dist/src/middleware.js.map +1 -1
  77. package/dist/src/pipeline/add-pieces.d.ts +3 -3
  78. package/dist/src/pipeline/add-pieces.d.ts.map +1 -1
  79. package/dist/src/pipeline/add-pieces.js +15 -7
  80. package/dist/src/pipeline/add-pieces.js.map +1 -1
  81. package/dist/src/pipeline/create-datasets.d.ts +4 -4
  82. package/dist/src/pipeline/create-datasets.d.ts.map +1 -1
  83. package/dist/src/pipeline/create-datasets.js +7 -14
  84. package/dist/src/pipeline/create-datasets.js.map +1 -1
  85. package/dist/src/types.d.ts +6 -7
  86. package/dist/src/types.d.ts.map +1 -1
  87. package/dist/src/utils.d.ts +7 -10
  88. package/dist/src/utils.d.ts.map +1 -1
  89. package/dist/src/utils.js +21 -2
  90. package/dist/src/utils.js.map +1 -1
  91. package/package.json +3 -3
  92. package/readme.md +98 -13
  93. package/src/api.ts +220 -0
  94. package/src/cli.ts +2 -0
  95. package/src/commands/datasets.ts +24 -36
  96. package/src/commands/providers.ts +34 -42
  97. package/src/commands/repair.ts +3 -3
  98. package/src/commands/replicate.ts +3 -3
  99. package/src/commands/second-copy.ts +187 -0
  100. package/src/commands/setup.ts +12 -35
  101. package/src/db/dedupe-cids.ts +7 -24
  102. package/src/db/find-repair-dataset.ts +17 -28
  103. package/src/db/get-active-data-set-ids.ts +29 -0
  104. package/src/db/get-alternate-provider-urls.ts +43 -0
  105. package/src/db/get-pieces.ts +82 -214
  106. package/src/db/get-repair-provider.ts +10 -30
  107. package/src/db/repair-create.ts +5 -3
  108. package/src/db/replicate-create.ts +15 -21
  109. package/src/db/second-copy-create.ts +147 -0
  110. package/src/local-schema.ts +3 -0
  111. package/src/middleware.ts +7 -11
  112. package/src/pipeline/add-pieces.ts +21 -11
  113. package/src/pipeline/create-datasets.ts +9 -29
  114. package/src/types.ts +9 -7
  115. package/src/utils.ts +22 -4
  116. package/dist/src/db/find-providers-by-cid.d.ts +0 -9
  117. package/dist/src/db/find-providers-by-cid.d.ts.map +0 -1
  118. package/dist/src/db/find-providers-by-cid.js +0 -43
  119. package/dist/src/db/find-providers-by-cid.js.map +0 -1
  120. package/dist/src/indexer-schema.d.ts +0 -549
  121. package/dist/src/indexer-schema.d.ts.map +0 -1
  122. package/dist/src/indexer-schema.js +0 -56
  123. package/dist/src/indexer-schema.js.map +0 -1
  124. package/src/db/find-providers-by-cid.ts +0 -70
  125. package/src/indexer-schema.ts +0 -77
package/src/cli.ts CHANGED
@@ -4,6 +4,7 @@ import { datasets } from './commands/datasets.ts'
4
4
  import { providers } from './commands/providers.ts'
5
5
  import { repair } from './commands/repair.ts'
6
6
  import { replicate } from './commands/replicate.ts'
7
+ import { secondCopy } from './commands/second-copy.ts'
7
8
  import { sessionKey } from './commands/session-key.ts'
8
9
  import { setup } from './commands/setup.ts'
9
10
  import { wallet } from './commands/wallet.ts'
@@ -18,6 +19,7 @@ cli.command(setup)
18
19
  cli.command(wallet)
19
20
  cli.command(repair)
20
21
  cli.command(replicate)
22
+ cli.command(secondCopy)
21
23
  cli.command(datasets)
22
24
  cli.command(providers)
23
25
  cli.command(sessionKey)
@@ -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
 
@@ -1,4 +1,4 @@
1
- import { and, desc, eq, inArray, isNull } from 'drizzle-orm'
1
+ import { and, desc, eq, inArray } from 'drizzle-orm'
2
2
  import { Cli, z } from 'incur'
3
3
  import { isAddress } from 'viem'
4
4
  import { repairCreate } from '../db/repair-create.ts'
@@ -52,7 +52,7 @@ repair.command('list', {
52
52
  try {
53
53
  const localSchema = c.var.localDb._.fullSchema
54
54
  const repairs = await c.var.localDb.query.repairs.findMany({
55
- where: isNull(localSchema.repairs.repairDataSetId),
55
+ where: eq(localSchema.repairs.kind, 'repair'),
56
56
  orderBy: [desc(localSchema.repairs.createdAt)],
57
57
  with: {
58
58
  operations: true,
@@ -147,7 +147,7 @@ repair.command('run', {
147
147
  where: and(
148
148
  eq(schema.repairs.id, c.args.repairId),
149
149
  inArray(schema.repairs.status, ['pending', 'failed']),
150
- isNull(schema.repairs.repairDataSetId)
150
+ eq(schema.repairs.kind, 'repair')
151
151
  ),
152
152
  })
153
153
  if (!repair) {
@@ -1,4 +1,4 @@
1
- import { and, desc, eq, inArray, isNotNull } from 'drizzle-orm'
1
+ import { and, desc, eq, inArray } from 'drizzle-orm'
2
2
  import { Cli, z } from 'incur'
3
3
  import { isAddress } from 'viem'
4
4
  import { repairDelete } from '../db/repair-delete.ts'
@@ -52,7 +52,7 @@ replicate.command('list', {
52
52
  try {
53
53
  const localSchema = c.var.localDb._.fullSchema
54
54
  const replications = await c.var.localDb.query.repairs.findMany({
55
- where: isNotNull(localSchema.repairs.repairDataSetId),
55
+ where: eq(localSchema.repairs.kind, 'replicate'),
56
56
  orderBy: [desc(localSchema.repairs.createdAt)],
57
57
  with: {
58
58
  operations: true,
@@ -148,7 +148,7 @@ replicate.command('run', {
148
148
  where: and(
149
149
  eq(schema.repairs.id, c.args.replicateId),
150
150
  inArray(schema.repairs.status, ['pending', 'failed']),
151
- isNotNull(schema.repairs.repairDataSetId)
151
+ eq(schema.repairs.kind, 'replicate')
152
152
  ),
153
153
  })
154
154
  if (!repair) {
@@ -0,0 +1,187 @@
1
+ import { and, desc, eq, inArray } from 'drizzle-orm'
2
+ import { Cli, z } from 'incur'
3
+ import { isAddress } from 'viem'
4
+ import { repairDelete } from '../db/repair-delete.ts'
5
+ import { secondCopyCreate } from '../db/second-copy-create.ts'
6
+ import { contextMiddleware, contextSchema } from '../middleware.ts'
7
+ import { runAddPieces } from '../pipeline/add-pieces.ts'
8
+ import { ensureRepairDataset } from '../pipeline/create-datasets.ts'
9
+ import { globalOptions } from '../utils.ts'
10
+
11
+ export const secondCopy = Cli.create('second-copy', {
12
+ description: 'Second-copy piece commands',
13
+ vars: contextSchema,
14
+ })
15
+
16
+ secondCopy.command('create', {
17
+ description: 'Create a second-copy job from a pieces file',
18
+ options: globalOptions.extend({
19
+ piecesFile: z.string().describe('Path to JSON file with aggregates[].piece_cid'),
20
+ sourceProviderUrl: z.string().url().describe('Source provider URL to pull pieces from'),
21
+ targetProviderId: z.coerce.bigint().describe('Target provider ID for second-copy'),
22
+ }),
23
+ middleware: [contextMiddleware],
24
+ run: async (c) => {
25
+ try {
26
+ const { piecesFile, sourceProviderUrl, targetProviderId } = c.options
27
+
28
+ const secondCopyId = await secondCopyCreate({
29
+ ...c.var,
30
+ piecesFile,
31
+ sourceProviderUrl,
32
+ targetProviderId,
33
+ })
34
+
35
+ return c.ok({
36
+ secondCopyId,
37
+ })
38
+ } catch (error) {
39
+ console.error(error)
40
+ return c.error({
41
+ code: 'SECOND_COPY_FAILED',
42
+ message: error instanceof Error ? error.message : 'Failed to create the second-copy job',
43
+ retryable: true,
44
+ })
45
+ }
46
+ },
47
+ })
48
+
49
+ secondCopy.command('list', {
50
+ description: 'List all second-copy jobs',
51
+ options: globalOptions,
52
+ middleware: [contextMiddleware],
53
+ run: async (c) => {
54
+ try {
55
+ const localSchema = c.var.localDb._.fullSchema
56
+ const jobs = await c.var.localDb.query.repairs.findMany({
57
+ where: eq(localSchema.repairs.kind, 'second_copy'),
58
+ orderBy: [desc(localSchema.repairs.createdAt)],
59
+ with: {
60
+ operations: true,
61
+ },
62
+ })
63
+
64
+ const secondCopies = jobs.map((repair) => {
65
+ const { operations, ...job } = repair
66
+ return {
67
+ id: job.id,
68
+ status: job.status,
69
+ sourceProviderUrl: job.sourceProviderUrl,
70
+ targetProviderId: job.targetProviderId,
71
+ targetProviderUrl: job.targetProviderUrl,
72
+ targetDataSetId: job.targetDataSetId,
73
+ blockNumber: job.blockNumber,
74
+ operations: operations.length,
75
+ pending: operations.filter((operation) => operation.status === 'pending').length,
76
+ failed: operations.filter((operation) => operation.status === 'failed').length,
77
+ completed: operations.filter((operation) => operation.status === 'completed').length,
78
+ skipped: operations.filter((operation) => operation.status === 'skipped').length,
79
+ }
80
+ })
81
+
82
+ return c.ok({
83
+ secondCopies,
84
+ })
85
+ } catch (error) {
86
+ console.error(error)
87
+ return c.error({
88
+ code: 'SECOND_COPY_FAILED',
89
+ message: error instanceof Error ? error.message : 'Failed to list second-copy jobs',
90
+ retryable: true,
91
+ })
92
+ }
93
+ },
94
+ })
95
+
96
+ secondCopy.command('delete', {
97
+ description: 'Delete a second-copy job',
98
+ args: z.object({
99
+ secondCopyId: z.coerce.number().describe('Second-copy ID to delete'),
100
+ }),
101
+ options: globalOptions,
102
+ middleware: [contextMiddleware],
103
+ run: async (c) => {
104
+ try {
105
+ const { deleted, operationsDeleted } = await repairDelete({
106
+ localDb: c.var.localDb,
107
+ repairId: c.args.secondCopyId,
108
+ })
109
+
110
+ if (!deleted) {
111
+ return c.error({
112
+ code: 'SECOND_COPY_NOT_FOUND',
113
+ message: 'Second-copy job not found',
114
+ retryable: false,
115
+ })
116
+ }
117
+
118
+ return c.ok({
119
+ secondCopyId: c.args.secondCopyId,
120
+ operationsDeleted,
121
+ })
122
+ } catch (error) {
123
+ console.error(error)
124
+ return c.error({
125
+ code: 'SECOND_COPY_FAILED',
126
+ message: error instanceof Error ? error.message : 'Failed to delete the second-copy job',
127
+ retryable: true,
128
+ })
129
+ }
130
+ },
131
+ })
132
+
133
+ secondCopy.command('run', {
134
+ description: 'Run a second-copy job',
135
+ args: z.object({
136
+ secondCopyId: z.coerce.number().describe('Second-copy ID to run'),
137
+ }),
138
+ options: globalOptions.extend({
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'),
141
+ payer: z.string().refine(isAddress, 'Invalid address').optional().describe('Payer address'),
142
+ }),
143
+ middleware: [contextMiddleware],
144
+ run: async (c) => {
145
+ try {
146
+ const schema = c.var.localDb._.fullSchema
147
+ const payer = c.options.payer ?? c.var.client.account.address
148
+ const repair = await c.var.localDb.query.repairs.findFirst({
149
+ where: and(
150
+ eq(schema.repairs.id, c.args.secondCopyId),
151
+ inArray(schema.repairs.status, ['pending', 'failed']),
152
+ eq(schema.repairs.kind, 'second_copy')
153
+ ),
154
+ })
155
+ if (!repair) {
156
+ return c.error({
157
+ code: 'SECOND_COPY_NOT_FOUND',
158
+ message: 'Second-copy job not found, it may have already been run or completed',
159
+ retryable: false,
160
+ })
161
+ }
162
+
163
+ await ensureRepairDataset({
164
+ ...c.var,
165
+ repair,
166
+ payer,
167
+ })
168
+
169
+ await runAddPieces({
170
+ ...c.var,
171
+ repair,
172
+ concurrency: c.options.concurrency,
173
+ batchSize: c.options.batchSize,
174
+ })
175
+ return c.ok({
176
+ secondCopyId: repair.id,
177
+ })
178
+ } catch (error) {
179
+ console.error(error)
180
+ return c.error({
181
+ code: 'SECOND_COPY_FAILED',
182
+ message: error instanceof Error ? error.message : 'Failed to run the second-copy job',
183
+ retryable: true,
184
+ })
185
+ }
186
+ },
187
+ })
@@ -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
+ }