@filoz/repair-cli 0.2.1 → 0.4.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 +1 -1
- package/dist/src/cli.js +4 -0
- package/dist/src/cli.js.map +1 -1
- package/dist/src/commands/datasets.d.ts +1 -0
- package/dist/src/commands/datasets.d.ts.map +1 -1
- package/dist/src/commands/datasets.js +5 -5
- package/dist/src/commands/datasets.js.map +1 -1
- package/dist/src/commands/providers.d.ts +1 -0
- package/dist/src/commands/providers.d.ts.map +1 -1
- package/dist/src/commands/repair.d.ts +1 -0
- package/dist/src/commands/repair.d.ts.map +1 -1
- package/dist/src/commands/repair.js +7 -3
- package/dist/src/commands/repair.js.map +1 -1
- package/dist/src/commands/replicate.d.ts +1 -0
- package/dist/src/commands/replicate.d.ts.map +1 -1
- package/dist/src/commands/replicate.js +7 -3
- package/dist/src/commands/replicate.js.map +1 -1
- package/dist/src/commands/second-copy.d.ts +25 -0
- package/dist/src/commands/second-copy.d.ts.map +1 -0
- package/dist/src/commands/second-copy.js +175 -0
- package/dist/src/commands/second-copy.js.map +1 -0
- package/dist/src/commands/session-key.d.ts +25 -0
- package/dist/src/commands/session-key.d.ts.map +1 -0
- package/dist/src/commands/session-key.js +114 -0
- package/dist/src/commands/session-key.js.map +1 -0
- package/dist/src/commands/wallet.d.ts +1 -0
- package/dist/src/commands/wallet.d.ts.map +1 -1
- package/dist/src/commands/wallet.js +75 -26
- package/dist/src/commands/wallet.js.map +1 -1
- package/dist/src/db/dedupe-cids.d.ts +1 -1
- package/dist/src/db/repair-create.d.ts.map +1 -1
- package/dist/src/db/repair-create.js +2 -0
- 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 +2 -0
- package/dist/src/db/replicate-create.js.map +1 -1
- package/dist/src/db/second-copy-create.d.ts +10 -0
- package/dist/src/db/second-copy-create.d.ts.map +1 -0
- package/dist/src/db/second-copy-create.js +104 -0
- package/dist/src/db/second-copy-create.js.map +1 -0
- package/dist/src/db/update-operation.d.ts +2 -2
- package/dist/src/db/update-operation.d.ts.map +1 -1
- package/dist/src/db/update-operation.js +2 -2
- package/dist/src/db/upsert-operations.js +1 -1
- package/dist/src/local-schema.d.ts +48 -11
- package/dist/src/local-schema.d.ts.map +1 -1
- package/dist/src/local-schema.js +3 -1
- package/dist/src/local-schema.js.map +1 -1
- package/dist/src/middleware.d.ts +2 -0
- package/dist/src/middleware.d.ts.map +1 -1
- package/dist/src/middleware.js +2 -0
- package/dist/src/middleware.js.map +1 -1
- package/dist/src/pipeline/add-pieces.d.ts.map +1 -1
- package/dist/src/pipeline/add-pieces.js +17 -3
- package/dist/src/pipeline/add-pieces.js.map +1 -1
- package/dist/src/pipeline/create-datasets.d.ts +4 -2
- package/dist/src/pipeline/create-datasets.d.ts.map +1 -1
- package/dist/src/pipeline/create-datasets.js +5 -5
- package/dist/src/pipeline/create-datasets.js.map +1 -1
- package/dist/src/utils.d.ts +34 -30
- package/dist/src/utils.d.ts.map +1 -1
- package/dist/src/utils.js +68 -3
- package/dist/src/utils.js.map +1 -1
- package/package.json +1 -1
- package/readme.md +123 -4
- package/src/cli.ts +4 -0
- package/src/commands/datasets.ts +7 -5
- package/src/commands/repair.ts +7 -3
- package/src/commands/replicate.ts +7 -3
- package/src/commands/second-copy.ts +187 -0
- package/src/commands/session-key.ts +121 -0
- package/src/commands/wallet.ts +75 -26
- package/src/db/repair-create.ts +2 -0
- package/src/db/replicate-create.ts +2 -0
- package/src/db/second-copy-create.ts +147 -0
- package/src/db/update-operation.ts +3 -3
- package/src/db/upsert-operations.ts +1 -1
- package/src/local-schema.ts +4 -7
- package/src/middleware.ts +2 -0
- package/src/pipeline/add-pieces.ts +20 -4
- package/src/pipeline/create-datasets.ts +20 -5
- package/src/utils.ts +95 -5
|
@@ -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
|
+
})
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import * as p from '@clack/prompts'
|
|
2
|
+
import * as SessionKey from '@filoz/synapse-core/session-key'
|
|
3
|
+
import { Cli, z } from 'incur'
|
|
4
|
+
import { type Address, isAddress } from 'viem'
|
|
5
|
+
import { contextMiddleware, contextSchema } from '../middleware.ts'
|
|
6
|
+
import { globalOptions, hashLink } from '../utils.ts'
|
|
7
|
+
|
|
8
|
+
const sessionKeyAddressArgs = z.object({
|
|
9
|
+
address: z.string().refine(isAddress, 'Invalid session key address').describe('Session key address'),
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
export const sessionKey = Cli.create('session-key', {
|
|
13
|
+
description: 'Session key commands',
|
|
14
|
+
vars: contextSchema,
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
sessionKey.command('approve', {
|
|
18
|
+
description: 'Approve a session key for storage operations',
|
|
19
|
+
args: sessionKeyAddressArgs,
|
|
20
|
+
options: globalOptions.extend({
|
|
21
|
+
expiresInDays: z.coerce.number().gt(0).default(100).describe('Session key expiry duration in days'),
|
|
22
|
+
origin: z.string().optional().describe('Origin recorded on-chain for the authorization'),
|
|
23
|
+
}),
|
|
24
|
+
middleware: [contextMiddleware],
|
|
25
|
+
outputPolicy: 'agent-only',
|
|
26
|
+
run: async (c) => {
|
|
27
|
+
const { client, chain, source, isInteractive } = c.var
|
|
28
|
+
const address = c.args.address as Address
|
|
29
|
+
const expiresAt = BigInt(Math.floor(Date.now() / 1000 + c.options.expiresInDays * 24 * 60 * 60))
|
|
30
|
+
const spinner = p.spinner()
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
if (isInteractive) {
|
|
34
|
+
spinner.start(`Approving session key ${address}...`)
|
|
35
|
+
}
|
|
36
|
+
const result = await SessionKey.loginSync(client, {
|
|
37
|
+
address,
|
|
38
|
+
expiresAt,
|
|
39
|
+
origin: c.options.origin ?? source,
|
|
40
|
+
onHash: (hash) => {
|
|
41
|
+
if (isInteractive) {
|
|
42
|
+
spinner.message(`Waiting for tx ${hashLink(hash, chain)} to be mined...`)
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
if (isInteractive) {
|
|
48
|
+
spinner.stop(`Session key ${address} approved successfully.`)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return c.ok({
|
|
52
|
+
address,
|
|
53
|
+
expiresAt: expiresAt.toString(),
|
|
54
|
+
transactionHash: result.receipt.transactionHash,
|
|
55
|
+
})
|
|
56
|
+
} catch (error) {
|
|
57
|
+
const msg = error instanceof Error ? error.message : 'Failed to approve session key'
|
|
58
|
+
if (isInteractive) {
|
|
59
|
+
spinner.error(msg)
|
|
60
|
+
}
|
|
61
|
+
if (c.options.debug) {
|
|
62
|
+
console.error(error)
|
|
63
|
+
}
|
|
64
|
+
return c.error({
|
|
65
|
+
code: 'FAILED_TO_APPROVE_SESSION_KEY',
|
|
66
|
+
message: msg,
|
|
67
|
+
})
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
sessionKey.command('revoke', {
|
|
73
|
+
description: 'Revoke a session key for storage operations',
|
|
74
|
+
args: sessionKeyAddressArgs,
|
|
75
|
+
options: globalOptions.extend({
|
|
76
|
+
origin: z.string().optional().describe('Origin recorded on-chain for the revocation'),
|
|
77
|
+
}),
|
|
78
|
+
middleware: [contextMiddleware],
|
|
79
|
+
outputPolicy: 'agent-only',
|
|
80
|
+
run: async (c) => {
|
|
81
|
+
const { client, chain, source, isInteractive } = c.var
|
|
82
|
+
const address = c.args.address as Address
|
|
83
|
+
const spinner = p.spinner()
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
if (isInteractive) {
|
|
87
|
+
spinner.start(`Revoking session key ${address}...`)
|
|
88
|
+
}
|
|
89
|
+
const result = await SessionKey.revokeSync(client, {
|
|
90
|
+
address,
|
|
91
|
+
origin: c.options.origin ?? source,
|
|
92
|
+
onHash: (hash) => {
|
|
93
|
+
if (isInteractive) {
|
|
94
|
+
spinner.message(`Waiting for tx ${hashLink(hash, chain)} to be mined...`)
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
if (isInteractive) {
|
|
100
|
+
spinner.stop(`Session key ${address} revoked successfully.`)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return c.ok({
|
|
104
|
+
address,
|
|
105
|
+
transactionHash: result.receipt.transactionHash,
|
|
106
|
+
})
|
|
107
|
+
} catch (error) {
|
|
108
|
+
const msg = error instanceof Error ? error.message : 'Failed to revoke session key'
|
|
109
|
+
if (isInteractive) {
|
|
110
|
+
spinner.error(msg)
|
|
111
|
+
}
|
|
112
|
+
if (c.options.debug) {
|
|
113
|
+
console.error(error)
|
|
114
|
+
}
|
|
115
|
+
return c.error({
|
|
116
|
+
code: 'FAILED_TO_REVOKE_SESSION_KEY',
|
|
117
|
+
message: msg,
|
|
118
|
+
})
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
})
|
package/src/commands/wallet.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/** biome-ignore-all lint/suspicious/noConsole: cli */
|
|
2
|
+
import * as p from '@clack/prompts'
|
|
2
3
|
import { calibration } from '@filoz/synapse-core/chains'
|
|
3
4
|
import * as ERC20 from '@filoz/synapse-core/erc20'
|
|
4
5
|
import * as Pay from '@filoz/synapse-core/pay'
|
|
5
6
|
import { claimTokens, formatBalance, formatFraction, parseUnits } from '@filoz/synapse-core/utils'
|
|
6
7
|
import { Cli, z } from 'incur'
|
|
8
|
+
import { isAddress } from 'viem'
|
|
7
9
|
import { getBalance, waitForTransactionReceipt } from 'viem/actions'
|
|
8
10
|
import { contextMiddleware, contextSchema } from '../middleware.ts'
|
|
9
11
|
import { globalOptions, hashLink } from '../utils.ts'
|
|
@@ -17,8 +19,9 @@ wallet.command('fund', {
|
|
|
17
19
|
description: 'Fund a calibration wallet from a faucet',
|
|
18
20
|
options: globalOptions,
|
|
19
21
|
middleware: [contextMiddleware],
|
|
20
|
-
|
|
21
|
-
|
|
22
|
+
outputPolicy: 'agent-only',
|
|
23
|
+
run: async (c) => {
|
|
24
|
+
const { client, chain, isInteractive } = c.var
|
|
22
25
|
|
|
23
26
|
if (chain.id !== calibration.id) {
|
|
24
27
|
return c.error({
|
|
@@ -27,28 +30,41 @@ wallet.command('fund', {
|
|
|
27
30
|
})
|
|
28
31
|
}
|
|
29
32
|
|
|
30
|
-
|
|
33
|
+
const spinner = p.spinner()
|
|
31
34
|
try {
|
|
35
|
+
if (isInteractive) {
|
|
36
|
+
spinner.start('Funding wallet...')
|
|
37
|
+
}
|
|
32
38
|
const hashes = await claimTokens({ address: client.account.address })
|
|
33
39
|
|
|
34
|
-
|
|
40
|
+
if (isInteractive) {
|
|
41
|
+
spinner.message(`Waiting for tx ${hashLink(hashes[0].tx_hash, chain)} to be mined...`)
|
|
42
|
+
}
|
|
35
43
|
await waitForTransactionReceipt(client, {
|
|
36
44
|
hash: hashes[0].tx_hash,
|
|
37
45
|
})
|
|
38
46
|
const balance = await getBalance(client, {
|
|
39
47
|
address: client.account.address,
|
|
40
48
|
})
|
|
41
|
-
|
|
49
|
+
if (isInteractive) {
|
|
50
|
+
spinner.stop('Wallet funded successfully.')
|
|
51
|
+
}
|
|
52
|
+
return c.ok({
|
|
42
53
|
address: client.account.address,
|
|
43
54
|
balance: formatBalance({ value: balance }),
|
|
44
|
-
|
|
55
|
+
transactionHash: hashes[0].tx_hash,
|
|
56
|
+
})
|
|
45
57
|
} catch (error) {
|
|
58
|
+
const msg = error instanceof Error ? error.message : 'Failed to fund wallet'
|
|
59
|
+
if (isInteractive) {
|
|
60
|
+
spinner.error(msg)
|
|
61
|
+
}
|
|
46
62
|
if (c.options.debug) {
|
|
47
63
|
console.error(error)
|
|
48
64
|
}
|
|
49
65
|
return c.error({
|
|
50
66
|
code: 'FAILED_TO_FUND_WALLET',
|
|
51
|
-
message:
|
|
67
|
+
message: msg,
|
|
52
68
|
})
|
|
53
69
|
}
|
|
54
70
|
},
|
|
@@ -56,23 +72,26 @@ wallet.command('fund', {
|
|
|
56
72
|
|
|
57
73
|
wallet.command('balance', {
|
|
58
74
|
description: 'Get wallet and pay account summary',
|
|
59
|
-
options: globalOptions
|
|
75
|
+
options: globalOptions.extend({
|
|
76
|
+
address: z.string().refine(isAddress, 'Invalid address').optional().describe('Address to get balance for'),
|
|
77
|
+
}),
|
|
60
78
|
middleware: [contextMiddleware],
|
|
61
79
|
async run(c) {
|
|
62
80
|
const { client } = c.var
|
|
81
|
+
const address = c.options.address ?? client.account.address
|
|
63
82
|
const balanceFIL = await getBalance(client, {
|
|
64
|
-
address
|
|
83
|
+
address,
|
|
65
84
|
})
|
|
66
85
|
|
|
67
86
|
const balanceUSDFC = await ERC20.balance(client, {
|
|
68
|
-
address
|
|
87
|
+
address,
|
|
69
88
|
})
|
|
70
89
|
|
|
71
90
|
const summary = await Pay.getAccountSummary(client, {
|
|
72
|
-
address
|
|
91
|
+
address,
|
|
73
92
|
})
|
|
74
93
|
return {
|
|
75
|
-
address
|
|
94
|
+
address,
|
|
76
95
|
fil: formatBalance({ value: balanceFIL }),
|
|
77
96
|
usdfc: formatBalance({ value: balanceUSDFC.value }),
|
|
78
97
|
pay: {
|
|
@@ -99,27 +118,42 @@ wallet.command('deposit', {
|
|
|
99
118
|
}),
|
|
100
119
|
options: globalOptions,
|
|
101
120
|
middleware: [contextMiddleware],
|
|
102
|
-
|
|
103
|
-
|
|
121
|
+
outputPolicy: 'agent-only',
|
|
122
|
+
run: async (c) => {
|
|
123
|
+
const { client, chain, isInteractive } = c.var
|
|
124
|
+
const spinner = p.spinner()
|
|
104
125
|
|
|
105
126
|
try {
|
|
106
|
-
|
|
127
|
+
if (isInteractive) {
|
|
128
|
+
spinner.start(`Depositing ${c.args.amount} USDFC to pay account...`)
|
|
129
|
+
}
|
|
107
130
|
const hash = await Pay.depositAndApprove(client, {
|
|
108
131
|
amount: parseUnits(c.args.amount),
|
|
109
132
|
})
|
|
110
|
-
|
|
133
|
+
if (isInteractive) {
|
|
134
|
+
spinner.message(`Waiting for tx ${hashLink(hash, chain)} to be mined...`)
|
|
135
|
+
}
|
|
111
136
|
await waitForTransactionReceipt(client, {
|
|
112
137
|
hash,
|
|
113
138
|
})
|
|
114
|
-
|
|
115
|
-
|
|
139
|
+
if (isInteractive) {
|
|
140
|
+
spinner.stop('Deposit successful.')
|
|
141
|
+
}
|
|
142
|
+
return c.ok({
|
|
143
|
+
amount: c.args.amount,
|
|
144
|
+
transactionHash: hash,
|
|
145
|
+
})
|
|
116
146
|
} catch (error) {
|
|
147
|
+
const msg = error instanceof Error ? error.message : 'Failed to deposit'
|
|
148
|
+
if (isInteractive) {
|
|
149
|
+
spinner.error(msg)
|
|
150
|
+
}
|
|
117
151
|
if (c.options.debug) {
|
|
118
152
|
console.error(error)
|
|
119
153
|
}
|
|
120
154
|
return c.error({
|
|
121
155
|
code: 'FAILED_TO_DEPOSIT',
|
|
122
|
-
message:
|
|
156
|
+
message: msg,
|
|
123
157
|
})
|
|
124
158
|
}
|
|
125
159
|
},
|
|
@@ -132,27 +166,42 @@ wallet.command('withdraw', {
|
|
|
132
166
|
}),
|
|
133
167
|
options: globalOptions,
|
|
134
168
|
middleware: [contextMiddleware],
|
|
135
|
-
|
|
136
|
-
|
|
169
|
+
outputPolicy: 'agent-only',
|
|
170
|
+
run: async (c) => {
|
|
171
|
+
const { client, chain, isInteractive } = c.var
|
|
172
|
+
const spinner = p.spinner()
|
|
137
173
|
|
|
138
174
|
try {
|
|
139
|
-
|
|
175
|
+
if (isInteractive) {
|
|
176
|
+
spinner.start(`Withdrawing ${c.args.amount} USDFC from pay account...`)
|
|
177
|
+
}
|
|
140
178
|
const hash = await Pay.withdraw(client, {
|
|
141
179
|
amount: parseUnits(c.args.amount),
|
|
142
180
|
})
|
|
143
|
-
|
|
181
|
+
if (isInteractive) {
|
|
182
|
+
spinner.message(`Waiting for tx ${hashLink(hash, chain)} to be mined...`)
|
|
183
|
+
}
|
|
144
184
|
await waitForTransactionReceipt(client, {
|
|
145
185
|
hash,
|
|
146
186
|
})
|
|
147
|
-
|
|
148
|
-
|
|
187
|
+
if (isInteractive) {
|
|
188
|
+
spinner.stop('Withdrawal successful.')
|
|
189
|
+
}
|
|
190
|
+
return c.ok({
|
|
191
|
+
amount: c.args.amount,
|
|
192
|
+
transactionHash: hash,
|
|
193
|
+
})
|
|
149
194
|
} catch (error) {
|
|
195
|
+
const msg = error instanceof Error ? error.message : 'Failed to withdraw'
|
|
196
|
+
if (isInteractive) {
|
|
197
|
+
spinner.error(msg)
|
|
198
|
+
}
|
|
150
199
|
if (c.options.debug) {
|
|
151
200
|
console.error(error)
|
|
152
201
|
}
|
|
153
202
|
return c.error({
|
|
154
203
|
code: 'FAILED_TO_WITHDRAW',
|
|
155
|
-
message:
|
|
204
|
+
message: msg,
|
|
156
205
|
})
|
|
157
206
|
}
|
|
158
207
|
},
|
package/src/db/repair-create.ts
CHANGED
|
@@ -46,8 +46,10 @@ export async function repairCreate(options: RepairCreateOptions): Promise<number
|
|
|
46
46
|
const [repair] = await localDb
|
|
47
47
|
.insert(localSchema.repairs)
|
|
48
48
|
.values({
|
|
49
|
+
kind: 'repair',
|
|
49
50
|
repairProviderId,
|
|
50
51
|
repairDataSetId: null,
|
|
52
|
+
sourceProviderUrl: null,
|
|
51
53
|
targetProviderId: targetProvider.providerId,
|
|
52
54
|
targetProviderUrl: targetProvider.serviceUrl,
|
|
53
55
|
targetDataSetId: null,
|
|
@@ -69,8 +69,10 @@ export async function replicateCreate(options: ReplicateCreateOptions): Promise<
|
|
|
69
69
|
const [repair] = await localDb
|
|
70
70
|
.insert(localSchema.repairs)
|
|
71
71
|
.values({
|
|
72
|
+
kind: 'replicate',
|
|
72
73
|
repairProviderId: sourceProvider.providerId,
|
|
73
74
|
repairDataSetId: sourceDataSet.dataSetId,
|
|
75
|
+
sourceProviderUrl: sourceProvider.serviceUrl,
|
|
74
76
|
targetProviderId: targetProvider.providerId,
|
|
75
77
|
targetProviderUrl: targetProvider.serviceUrl,
|
|
76
78
|
targetDataSetId: null,
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises'
|
|
2
|
+
import { taskLog } from '@clack/prompts'
|
|
3
|
+
import { getBlockNumber } from 'viem/actions'
|
|
4
|
+
import { NoAlternateProviderError, RepairCreationError } from '../error.ts'
|
|
5
|
+
import type { OperationInsert } from '../local-schema.ts'
|
|
6
|
+
import type { Context } from '../types.ts'
|
|
7
|
+
import { getRepairProvider } from './get-repair-provider.ts'
|
|
8
|
+
|
|
9
|
+
const OPERATIONS_INSERT_BATCH_SIZE = 500
|
|
10
|
+
|
|
11
|
+
export interface SecondCopyCreateOptions extends Context {
|
|
12
|
+
piecesFile: string
|
|
13
|
+
sourceProviderUrl: string
|
|
14
|
+
targetProviderId: bigint
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type PiecesFile = {
|
|
18
|
+
aggregates: Array<{ piece_cid?: unknown }>
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Normalize a provider service URL for storage and equality checks.
|
|
23
|
+
*/
|
|
24
|
+
export function normalizeProviderUrl(url: string): string {
|
|
25
|
+
let parsed: URL
|
|
26
|
+
try {
|
|
27
|
+
parsed = new URL(url)
|
|
28
|
+
} catch {
|
|
29
|
+
throw new RepairCreationError(`Invalid source provider URL: ${url}`)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
|
33
|
+
throw new RepairCreationError(`Source provider URL must be http(s): ${url}`)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Drop trailing slash so endpoint comparisons are stable.
|
|
37
|
+
return parsed.toString().replace(/\/$/, '')
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Parse a second-copy pieces file and return unique aggregate piece CIDs.
|
|
42
|
+
*/
|
|
43
|
+
export async function parsePieceCidsFromFile(piecesFile: string): Promise<string[]> {
|
|
44
|
+
let raw: unknown
|
|
45
|
+
try {
|
|
46
|
+
raw = JSON.parse(await readFile(piecesFile, 'utf8'))
|
|
47
|
+
} catch (error) {
|
|
48
|
+
throw new RepairCreationError(
|
|
49
|
+
error instanceof Error ? `Failed to read pieces file: ${error.message}` : 'Failed to read pieces file'
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (raw == null || typeof raw !== 'object' || !Array.isArray((raw as PiecesFile).aggregates)) {
|
|
54
|
+
throw new RepairCreationError('Pieces file must contain an "aggregates" array')
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const cids = new Set<string>()
|
|
58
|
+
for (const aggregate of (raw as PiecesFile).aggregates) {
|
|
59
|
+
if (typeof aggregate?.piece_cid !== 'string' || aggregate.piece_cid.length === 0) {
|
|
60
|
+
throw new RepairCreationError('Each aggregate must include a non-empty "piece_cid" string')
|
|
61
|
+
}
|
|
62
|
+
cids.add(aggregate.piece_cid)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (cids.size === 0) {
|
|
66
|
+
throw new RepairCreationError('Pieces file contains no piece CIDs')
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return [...cids]
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Prepare a second-copy job from a pieces file and an explicit source provider URL.
|
|
74
|
+
*
|
|
75
|
+
* @param options - The options for creating a second-copy job.
|
|
76
|
+
* @returns The ID of the created local job.
|
|
77
|
+
*/
|
|
78
|
+
export async function secondCopyCreate(options: SecondCopyCreateOptions): Promise<number> {
|
|
79
|
+
const { indexerDb, localDb, piecesFile, sourceProviderUrl, targetProviderId, client } = options
|
|
80
|
+
const localSchema = localDb._.fullSchema
|
|
81
|
+
const now = Date.now()
|
|
82
|
+
const blockNumber = await getBlockNumber(client)
|
|
83
|
+
const normalizedSourceUrl = normalizeProviderUrl(sourceProviderUrl)
|
|
84
|
+
|
|
85
|
+
const log = taskLog({
|
|
86
|
+
title: 'Creating second-copy',
|
|
87
|
+
limit: 10,
|
|
88
|
+
retainLog: true,
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
const targetProvider = await getRepairProvider({
|
|
92
|
+
indexerDb,
|
|
93
|
+
providerId: targetProviderId,
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
if (!targetProvider) {
|
|
97
|
+
throw new NoAlternateProviderError(targetProviderId)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const normalizedTargetUrl = normalizeProviderUrl(targetProvider.serviceUrl)
|
|
101
|
+
if (normalizedSourceUrl === normalizedTargetUrl) {
|
|
102
|
+
throw new RepairCreationError('Source provider URL must differ from the target provider URL')
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const pieceCids = await parsePieceCidsFromFile(piecesFile)
|
|
106
|
+
log.message(`Loaded ${pieceCids.length} unique piece CIDs from ${piecesFile}`)
|
|
107
|
+
|
|
108
|
+
const [repair] = await localDb
|
|
109
|
+
.insert(localSchema.repairs)
|
|
110
|
+
.values({
|
|
111
|
+
kind: 'second_copy',
|
|
112
|
+
repairProviderId: 0n,
|
|
113
|
+
repairDataSetId: null,
|
|
114
|
+
sourceProviderUrl: normalizedSourceUrl,
|
|
115
|
+
targetProviderId: targetProvider.providerId,
|
|
116
|
+
targetProviderUrl: targetProvider.serviceUrl,
|
|
117
|
+
targetDataSetId: null,
|
|
118
|
+
blockNumber,
|
|
119
|
+
createdAt: now,
|
|
120
|
+
updatedAt: now,
|
|
121
|
+
})
|
|
122
|
+
.returning({ id: localSchema.repairs.id })
|
|
123
|
+
|
|
124
|
+
if (!repair) throw new RepairCreationError('Failed to create second-copy row')
|
|
125
|
+
|
|
126
|
+
let totalOperations = 0
|
|
127
|
+
for (let offset = 0; offset < pieceCids.length; offset += OPERATIONS_INSERT_BATCH_SIZE) {
|
|
128
|
+
const batchCids = pieceCids.slice(offset, offset + OPERATIONS_INSERT_BATCH_SIZE)
|
|
129
|
+
const operations: OperationInsert[] = batchCids.map((cid) => ({
|
|
130
|
+
repairId: repair.id,
|
|
131
|
+
type: 'add_piece',
|
|
132
|
+
status: 'pending',
|
|
133
|
+
cid,
|
|
134
|
+
metadata: {},
|
|
135
|
+
alternateProvider: normalizedSourceUrl,
|
|
136
|
+
createdAt: now,
|
|
137
|
+
updatedAt: now,
|
|
138
|
+
}))
|
|
139
|
+
|
|
140
|
+
await localDb.insert(localSchema.operations).values(operations)
|
|
141
|
+
totalOperations += operations.length
|
|
142
|
+
log.message(`Inserted ${operations.length} operations (${totalOperations}/${pieceCids.length})`)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
log.success(`Created second-copy ${repair.id} with ${totalOperations} operations`, { showLog: true })
|
|
146
|
+
return repair.id
|
|
147
|
+
}
|