@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.
- package/dist/package.json +3 -3
- package/dist/src/api.d.ts +35 -0
- package/dist/src/api.d.ts.map +1 -0
- package/dist/src/api.js +118 -0
- package/dist/src/api.js.map +1 -0
- package/dist/src/commands/datasets.d.ts +3 -5
- package/dist/src/commands/datasets.d.ts.map +1 -1
- package/dist/src/commands/datasets.js +24 -33
- package/dist/src/commands/datasets.js.map +1 -1
- package/dist/src/commands/providers.d.ts +3 -5
- package/dist/src/commands/providers.d.ts.map +1 -1
- package/dist/src/commands/providers.js +32 -33
- package/dist/src/commands/providers.js.map +1 -1
- package/dist/src/commands/repair.d.ts +3 -5
- package/dist/src/commands/repair.d.ts.map +1 -1
- package/dist/src/commands/replicate.d.ts +3 -5
- package/dist/src/commands/replicate.d.ts.map +1 -1
- package/dist/src/commands/second-copy.d.ts +3 -5
- package/dist/src/commands/second-copy.d.ts.map +1 -1
- package/dist/src/commands/session-key.d.ts +3 -5
- package/dist/src/commands/session-key.d.ts.map +1 -1
- package/dist/src/commands/setup.d.ts.map +1 -1
- package/dist/src/commands/setup.js +11 -32
- package/dist/src/commands/setup.js.map +1 -1
- package/dist/src/commands/wallet.d.ts +3 -5
- package/dist/src/commands/wallet.d.ts.map +1 -1
- package/dist/src/db/dedupe-cids.d.ts +6 -7
- package/dist/src/db/dedupe-cids.d.ts.map +1 -1
- package/dist/src/db/dedupe-cids.js +2 -14
- package/dist/src/db/dedupe-cids.js.map +1 -1
- package/dist/src/db/find-repair-dataset.d.ts +3 -3
- package/dist/src/db/find-repair-dataset.d.ts.map +1 -1
- package/dist/src/db/find-repair-dataset.js +16 -14
- package/dist/src/db/find-repair-dataset.js.map +1 -1
- package/dist/src/db/get-active-data-set-ids.d.ts +9 -0
- package/dist/src/db/get-active-data-set-ids.d.ts.map +1 -0
- package/dist/src/db/get-active-data-set-ids.js +9 -0
- package/dist/src/db/get-active-data-set-ids.js.map +1 -0
- package/dist/src/db/get-alternate-provider-urls.d.ts +6 -0
- package/dist/src/db/get-alternate-provider-urls.d.ts.map +1 -0
- package/dist/src/db/get-alternate-provider-urls.js +24 -0
- package/dist/src/db/get-alternate-provider-urls.js.map +1 -0
- package/dist/src/db/get-pieces.d.ts +8 -27
- package/dist/src/db/get-pieces.d.ts.map +1 -1
- package/dist/src/db/get-pieces.js +44 -97
- package/dist/src/db/get-pieces.js.map +1 -1
- package/dist/src/db/get-repair-provider.d.ts +2 -2
- package/dist/src/db/get-repair-provider.d.ts.map +1 -1
- package/dist/src/db/get-repair-provider.js +8 -20
- package/dist/src/db/get-repair-provider.js.map +1 -1
- package/dist/src/db/repair-create.js +3 -3
- package/dist/src/db/repair-create.js.map +1 -1
- package/dist/src/db/replicate-create.d.ts.map +1 -1
- package/dist/src/db/replicate-create.js +13 -21
- package/dist/src/db/replicate-create.js.map +1 -1
- package/dist/src/db/second-copy-create.js +2 -2
- package/dist/src/db/second-copy-create.js.map +1 -1
- package/dist/src/local-schema.d.ts +1 -1
- package/dist/src/middleware.d.ts +7 -11
- package/dist/src/middleware.d.ts.map +1 -1
- package/dist/src/middleware.js +6 -10
- package/dist/src/middleware.js.map +1 -1
- package/dist/src/pipeline/add-pieces.d.ts +3 -3
- package/dist/src/pipeline/add-pieces.d.ts.map +1 -1
- package/dist/src/pipeline/add-pieces.js +15 -7
- package/dist/src/pipeline/add-pieces.js.map +1 -1
- package/dist/src/pipeline/create-datasets.d.ts +4 -4
- package/dist/src/pipeline/create-datasets.d.ts.map +1 -1
- package/dist/src/pipeline/create-datasets.js +7 -14
- package/dist/src/pipeline/create-datasets.js.map +1 -1
- package/dist/src/types.d.ts +6 -7
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/utils.d.ts +7 -10
- package/dist/src/utils.d.ts.map +1 -1
- package/dist/src/utils.js +1 -2
- package/dist/src/utils.js.map +1 -1
- package/package.json +3 -3
- package/readme.md +16 -13
- package/src/api.ts +220 -0
- package/src/commands/datasets.ts +24 -36
- package/src/commands/providers.ts +34 -42
- package/src/commands/setup.ts +12 -35
- package/src/db/dedupe-cids.ts +7 -24
- package/src/db/find-repair-dataset.ts +17 -28
- package/src/db/get-active-data-set-ids.ts +29 -0
- package/src/db/get-alternate-provider-urls.ts +43 -0
- package/src/db/get-pieces.ts +82 -214
- package/src/db/get-repair-provider.ts +10 -30
- package/src/db/repair-create.ts +3 -3
- package/src/db/replicate-create.ts +13 -21
- package/src/db/second-copy-create.ts +2 -2
- package/src/middleware.ts +7 -11
- package/src/pipeline/add-pieces.ts +20 -10
- package/src/pipeline/create-datasets.ts +9 -29
- package/src/types.ts +9 -7
- package/src/utils.ts +2 -4
- package/dist/src/db/find-providers-by-cid.d.ts +0 -9
- package/dist/src/db/find-providers-by-cid.d.ts.map +0 -1
- package/dist/src/db/find-providers-by-cid.js +0 -43
- package/dist/src/db/find-providers-by-cid.js.map +0 -1
- package/dist/src/indexer-schema.d.ts +0 -549
- package/dist/src/indexer-schema.d.ts.map +0 -1
- package/dist/src/indexer-schema.js +0 -56
- package/dist/src/indexer-schema.js.map +0 -1
- package/src/db/find-providers-by-cid.ts +0 -70
- 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
|
+
}
|
package/src/commands/datasets.ts
CHANGED
|
@@ -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
|
|
26
|
+
const { api } = c.var
|
|
26
27
|
const payer = c.options.payer?.toLowerCase() ?? c.var.client.account.address.toLowerCase()
|
|
27
|
-
const
|
|
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
|
-
|
|
30
|
-
|
|
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:
|
|
41
|
+
provider: dataset.providerId == null ? null : serviceUrls.get(dataset.providerId),
|
|
52
42
|
pdpEndEpoch: dataset.pdpEndEpoch,
|
|
53
|
-
|
|
54
|
-
|
|
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
|
|
142
|
-
|
|
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
|
-
|
|
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
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
|
82
|
-
totalSize: formatBytesAsGb(stats?.totalSize ?? 0n),
|
|
74
|
+
...(stats && { pieceCount: stats.pieceCount, totalSize: formatBytesAsGb(stats.totalSize) }),
|
|
83
75
|
}
|
|
84
76
|
})
|
|
85
77
|
|
package/src/commands/setup.ts
CHANGED
|
@@ -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
|
|
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
|
-
//
|
|
48
|
-
const
|
|
49
|
-
message: 'Enter
|
|
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 || !
|
|
69
|
-
return `Invalid
|
|
46
|
+
if (!value || !validateHttpUrl(value)) {
|
|
47
|
+
return `Invalid http(s) URL!`
|
|
70
48
|
}
|
|
71
49
|
},
|
|
72
|
-
initialValue: config.get('
|
|
50
|
+
initialValue: config.get('apiUrl') ?? DEFAULT_API_URL,
|
|
73
51
|
withGuide: false,
|
|
74
52
|
})
|
|
75
|
-
if (p.isCancel(
|
|
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('
|
|
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)
|
package/src/db/dedupe-cids.ts
CHANGED
|
@@ -1,37 +1,20 @@
|
|
|
1
|
-
import { and, eq, inArray } from 'drizzle-orm'
|
|
2
1
|
import type { OperationSelect } from '../local-schema.ts'
|
|
3
|
-
import type {
|
|
2
|
+
import type { LocalDatabase } from '../types.ts'
|
|
4
3
|
import { upsertOperations } from './upsert-operations.ts'
|
|
5
4
|
|
|
6
|
-
export type
|
|
7
|
-
indexerDb: IndexerDatabase
|
|
5
|
+
export type DedupeCidsOptions = {
|
|
8
6
|
localDb: LocalDatabase
|
|
9
|
-
|
|
7
|
+
/** CIDs already in the target repair dataset. */
|
|
8
|
+
existingCids: ReadonlySet<string>
|
|
10
9
|
operations: OperationSelect[]
|
|
11
10
|
}
|
|
12
11
|
|
|
13
12
|
/**
|
|
14
|
-
*
|
|
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({
|
|
18
|
-
const
|
|
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
|
|
2
|
+
import { listDataSets } from '../api.ts'
|
|
3
|
+
import type { IndexerApi } from '../types.ts'
|
|
4
4
|
|
|
5
5
|
export type FindRepairDatasetOptions = {
|
|
6
|
-
|
|
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
|
|
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
|
-
|
|
18
|
+
api,
|
|
19
19
|
providerId,
|
|
20
20
|
payer,
|
|
21
21
|
source,
|
|
22
22
|
}: FindRepairDatasetOptions): Promise<bigint | null> {
|
|
23
|
-
const
|
|
23
|
+
const dataSets = await listDataSets(api, {
|
|
24
|
+
owner: payer.toLowerCase(),
|
|
25
|
+
providerId,
|
|
26
|
+
deleted: false,
|
|
27
|
+
withCdn: false,
|
|
28
|
+
})
|
|
24
29
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
|
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
|
+
}
|