@filoz/repair-cli 0.2.1 → 0.3.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 +2 -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 +4 -0
- 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 +4 -0
- package/dist/src/commands/replicate.js.map +1 -1
- 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/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 +8 -11
- package/dist/src/local-schema.d.ts.map +1 -1
- package/dist/src/local-schema.js +1 -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 +48 -3
- package/dist/src/utils.js.map +1 -1
- package/package.json +1 -1
- package/readme.md +40 -3
- package/src/cli.ts +2 -0
- package/src/commands/datasets.ts +7 -5
- package/src/commands/repair.ts +4 -0
- package/src/commands/replicate.ts +4 -0
- package/src/commands/session-key.ts +121 -0
- package/src/commands/wallet.ts +75 -26
- package/src/db/update-operation.ts +3 -3
- package/src/db/upsert-operations.ts +1 -1
- package/src/local-schema.ts +1 -7
- package/src/middleware.ts +2 -0
- package/src/pipeline/add-pieces.ts +19 -3
- package/src/pipeline/create-datasets.ts +20 -5
- package/src/utils.ts +75 -5
|
@@ -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
|
},
|
|
@@ -6,19 +6,19 @@ export type UpdateOperationOptions = {
|
|
|
6
6
|
localDb: LocalDatabase
|
|
7
7
|
operationId: number
|
|
8
8
|
status: localSchema.OperationStatus
|
|
9
|
-
|
|
9
|
+
txHash?: string | null
|
|
10
10
|
error?: string | null
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Updates an operation in the database.
|
|
15
15
|
*/
|
|
16
|
-
export async function updateOperation({ localDb, operationId, status,
|
|
16
|
+
export async function updateOperation({ localDb, operationId, status, txHash, error }: UpdateOperationOptions) {
|
|
17
17
|
await localDb
|
|
18
18
|
.update(localSchema.operations)
|
|
19
19
|
.set({
|
|
20
20
|
status,
|
|
21
|
-
|
|
21
|
+
txHash,
|
|
22
22
|
error: error ?? null,
|
|
23
23
|
updatedAt: Date.now(),
|
|
24
24
|
})
|
|
@@ -18,6 +18,6 @@ export async function upsertOperations({ localDb, operations }: UpsertOperations
|
|
|
18
18
|
.values(operations.map((operation) => ({ ...operation, updatedAt: now })))
|
|
19
19
|
.onConflictDoUpdate({
|
|
20
20
|
target: localDb._.fullSchema.operations.id,
|
|
21
|
-
set: buildConflictUpdateColumns(localSchema.operations, ['status', 'error', 'updatedAt', '
|
|
21
|
+
set: buildConflictUpdateColumns(localSchema.operations, ['status', 'error', 'updatedAt', 'txHash']),
|
|
22
22
|
})
|
|
23
23
|
}
|
package/src/local-schema.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { MetadataObject } from '@filoz/synapse-core'
|
|
2
|
-
import type * as SP from '@filoz/synapse-core/sp'
|
|
3
2
|
import { relations } from 'drizzle-orm'
|
|
4
3
|
import type { AnySQLiteColumn } from 'drizzle-orm/sqlite-core'
|
|
5
4
|
import * as t from 'drizzle-orm/sqlite-core'
|
|
@@ -10,11 +9,6 @@ export type RepairStatus = 'pending' | 'completed' | 'failed'
|
|
|
10
9
|
export type OperationStatus = 'pending' | 'completed' | 'failed' | 'skipped'
|
|
11
10
|
export type OperationType = 'create_dataset' | 'add_piece'
|
|
12
11
|
|
|
13
|
-
export type OperationResult = Omit<
|
|
14
|
-
SP.AddPiecesSuccess,
|
|
15
|
-
'txStatus' | 'addMessageOk' | 'piecesAdded' | 'pieceCount' | 'confirmedPieceIds'
|
|
16
|
-
>
|
|
17
|
-
|
|
18
12
|
/**
|
|
19
13
|
* Custom type for JSON
|
|
20
14
|
* It will be used to store JSON data in the database
|
|
@@ -74,7 +68,7 @@ export const operations = table('operations', {
|
|
|
74
68
|
cid: t.text().notNull(),
|
|
75
69
|
metadata: jsonType().$type<MetadataObject>().notNull(),
|
|
76
70
|
alternateProvider: t.text('alternate_provider').notNull(),
|
|
77
|
-
|
|
71
|
+
txHash: t.text('tx_hash'),
|
|
78
72
|
error: t.text(),
|
|
79
73
|
createdAt: t.integer('created_at').notNull(),
|
|
80
74
|
updatedAt: t.integer('updated_at').notNull(),
|
package/src/middleware.ts
CHANGED
|
@@ -13,6 +13,7 @@ export const contextSchema = z.object({
|
|
|
13
13
|
client: z.custom<Client<Transport, Chain, Account>>(),
|
|
14
14
|
chain: z.custom<Chain>(),
|
|
15
15
|
source: z.string(),
|
|
16
|
+
isInteractive: z.boolean(),
|
|
16
17
|
})
|
|
17
18
|
|
|
18
19
|
export const contextMiddleware = middleware<typeof contextSchema>(async (c, next) => {
|
|
@@ -38,6 +39,7 @@ export const contextMiddleware = middleware<typeof contextSchema>(async (c, next
|
|
|
38
39
|
c.set('client', client)
|
|
39
40
|
c.set('chain', chain)
|
|
40
41
|
c.set('source', source)
|
|
42
|
+
c.set('isInteractive', !c.agent && !c.formatExplicit)
|
|
41
43
|
await next()
|
|
42
44
|
|
|
43
45
|
localDb.$client.close()
|
|
@@ -9,7 +9,7 @@ import { repairUpdate } from '../db/repair-update.ts'
|
|
|
9
9
|
import { upsertOperations } from '../db/upsert-operations.ts'
|
|
10
10
|
import type { OperationSelect, RepairSelect } from '../local-schema.ts'
|
|
11
11
|
import type { IndexerDatabase, LocalDatabase, WalletClient } from '../types.ts'
|
|
12
|
-
import { excludeOperationsByCid, hashLink, operationsToPullPieces } from '../utils.ts'
|
|
12
|
+
import { completeConfirmedOperations, excludeOperationsByCid, hashLink, operationsToPullPieces } from '../utils.ts'
|
|
13
13
|
|
|
14
14
|
export type RunPullPiecesPhaseOptions = {
|
|
15
15
|
localDb: LocalDatabase
|
|
@@ -58,6 +58,15 @@ function createAddPiecesWorker({ localDb, indexerDb, repair, client, state, log
|
|
|
58
58
|
if (isRepair) {
|
|
59
59
|
operations = await dedupeCids({ indexerDb, localDb, dataSetId: dataset.dataSetId, operations })
|
|
60
60
|
}
|
|
61
|
+
// A previous run may have submitted a transaction and crashed before marking rows completed.
|
|
62
|
+
const operationsBeforeConfirmationCheck = operations.length
|
|
63
|
+
operations = await completeConfirmedOperations({ localDb, client, operations })
|
|
64
|
+
const confirmedOperations = operationsBeforeConfirmationCheck - operations.length
|
|
65
|
+
if (confirmedOperations > 0) {
|
|
66
|
+
state.completedOperations += confirmedOperations
|
|
67
|
+
completedOps += confirmedOperations
|
|
68
|
+
}
|
|
69
|
+
|
|
61
70
|
group.message(`Pulling ${operations.length} pieces...`)
|
|
62
71
|
// pull pieces
|
|
63
72
|
if (operations.length > 0) {
|
|
@@ -105,9 +114,16 @@ function createAddPiecesWorker({ localDb, indexerDb, repair, client, state, log
|
|
|
105
114
|
metadata: isRepair ? undefined : operation.metadata,
|
|
106
115
|
})),
|
|
107
116
|
})
|
|
117
|
+
await upsertOperations({
|
|
118
|
+
localDb,
|
|
119
|
+
operations: operations.map((operation) => ({
|
|
120
|
+
...operation,
|
|
121
|
+
txHash: addPiecesResult.txHash,
|
|
122
|
+
})),
|
|
123
|
+
})
|
|
108
124
|
|
|
109
125
|
group.message(`Waiting for add pieces ${hashLink(addPiecesResult.txHash, client.chain)}...`)
|
|
110
|
-
const
|
|
126
|
+
const waitForAddPieces = await SP.waitForAddPieces(addPiecesResult)
|
|
111
127
|
state.completedOperations += operations.length
|
|
112
128
|
completedOps += operations.length
|
|
113
129
|
await upsertOperations({
|
|
@@ -116,7 +132,7 @@ function createAddPiecesWorker({ localDb, indexerDb, repair, client, state, log
|
|
|
116
132
|
...operation,
|
|
117
133
|
status: 'completed',
|
|
118
134
|
error: null,
|
|
119
|
-
|
|
135
|
+
txHash: waitForAddPieces.txHash,
|
|
120
136
|
})),
|
|
121
137
|
})
|
|
122
138
|
}
|
|
@@ -2,6 +2,7 @@ import * as p from '@clack/prompts'
|
|
|
2
2
|
import * as SP from '@filoz/synapse-core/sp'
|
|
3
3
|
import { getPDPProvider } from '@filoz/synapse-core/sp-registry'
|
|
4
4
|
import { eq } from 'drizzle-orm'
|
|
5
|
+
import type { Address } from 'viem'
|
|
5
6
|
import { findRepairDataset } from '../db/find-repair-dataset.ts'
|
|
6
7
|
import { repairUpdate } from '../db/repair-update.ts'
|
|
7
8
|
import type { RepairSelect } from '../local-schema.ts'
|
|
@@ -14,6 +15,7 @@ export type EnsureRepairDatasetOptions = {
|
|
|
14
15
|
indexerDb: IndexerDatabase
|
|
15
16
|
client: WalletClient
|
|
16
17
|
repair: RepairSelect
|
|
18
|
+
payer: Address
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
/**
|
|
@@ -21,7 +23,14 @@ export type EnsureRepairDatasetOptions = {
|
|
|
21
23
|
*
|
|
22
24
|
* @param options - The options for ensuring the repair dataset.
|
|
23
25
|
*/
|
|
24
|
-
export async function ensureRepairDataset({
|
|
26
|
+
export async function ensureRepairDataset({
|
|
27
|
+
source,
|
|
28
|
+
localDb,
|
|
29
|
+
indexerDb,
|
|
30
|
+
client,
|
|
31
|
+
repair,
|
|
32
|
+
payer,
|
|
33
|
+
}: EnsureRepairDatasetOptions) {
|
|
25
34
|
const log = p.taskLog({
|
|
26
35
|
title: 'Ensuring repair dataset',
|
|
27
36
|
})
|
|
@@ -36,7 +45,7 @@ export async function ensureRepairDataset({ source, localDb, indexerDb, client,
|
|
|
36
45
|
const existingDatasetId = await findRepairDataset({
|
|
37
46
|
indexerDb,
|
|
38
47
|
providerId: repair.targetProviderId,
|
|
39
|
-
payer
|
|
48
|
+
payer,
|
|
40
49
|
source,
|
|
41
50
|
})
|
|
42
51
|
|
|
@@ -47,7 +56,7 @@ export async function ensureRepairDataset({ source, localDb, indexerDb, client,
|
|
|
47
56
|
const { txHash, statusUrl } = await SP.createDataSet(client, {
|
|
48
57
|
payee: provider.payee,
|
|
49
58
|
serviceURL: provider.pdp.serviceURL,
|
|
50
|
-
payer
|
|
59
|
+
payer,
|
|
51
60
|
cdn: false,
|
|
52
61
|
metadata: {
|
|
53
62
|
source,
|
|
@@ -74,7 +83,13 @@ export async function ensureRepairDataset({ source, localDb, indexerDb, client,
|
|
|
74
83
|
*
|
|
75
84
|
* @param options - The options for ensuring the replication dataset.
|
|
76
85
|
*/
|
|
77
|
-
export async function ensureReplicateDataset({
|
|
86
|
+
export async function ensureReplicateDataset({
|
|
87
|
+
localDb,
|
|
88
|
+
indexerDb,
|
|
89
|
+
client,
|
|
90
|
+
repair,
|
|
91
|
+
payer,
|
|
92
|
+
}: EnsureRepairDatasetOptions) {
|
|
78
93
|
const log = p.taskLog({
|
|
79
94
|
title: 'Ensuring replication dataset',
|
|
80
95
|
})
|
|
@@ -110,7 +125,7 @@ export async function ensureReplicateDataset({ localDb, indexerDb, client, repai
|
|
|
110
125
|
const { txHash, statusUrl } = await SP.createDataSet(client, {
|
|
111
126
|
payee: provider.payee,
|
|
112
127
|
serviceURL: provider.pdp.serviceURL,
|
|
113
|
-
payer
|
|
128
|
+
payer,
|
|
114
129
|
cdn: sourceDataSet.withCdn,
|
|
115
130
|
metadata: sourceDataSet.metadata ?? undefined,
|
|
116
131
|
})
|
package/src/utils.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import type { MetadataObject } from '@filoz/synapse-core'
|
|
2
1
|
import { type Chain, getChain } from '@filoz/synapse-core/chains'
|
|
3
2
|
import * as Piece from '@filoz/synapse-core/piece'
|
|
4
3
|
import type * as SP from '@filoz/synapse-core/sp'
|
|
5
|
-
import { getTableColumns, type SQL, sql } from 'drizzle-orm'
|
|
4
|
+
import { getTableColumns, inArray, type SQL, sql } from 'drizzle-orm'
|
|
6
5
|
import { drizzle } from 'drizzle-orm/libsql'
|
|
7
6
|
import type { PgTable } from 'drizzle-orm/pg-core'
|
|
8
7
|
import type { SQLiteTable } from 'drizzle-orm/sqlite-core'
|
|
@@ -11,12 +10,13 @@ import { Conf } from 'iso-conf'
|
|
|
11
10
|
import { request } from 'iso-web/http'
|
|
12
11
|
import pLocate from 'p-locate'
|
|
13
12
|
import terminalLink from 'terminal-link'
|
|
14
|
-
import { createWalletClient, type Hex, http } from 'viem'
|
|
13
|
+
import { createWalletClient, type Hash, type Hex, http, TransactionReceiptNotFoundError } from 'viem'
|
|
15
14
|
import { privateKeyToAccount } from 'viem/accounts'
|
|
15
|
+
import { getTransactionReceipt } from 'viem/actions'
|
|
16
16
|
import packageJson from '../package.json' with { type: 'json' }
|
|
17
17
|
import type { OperationSelect } from './local-schema.ts'
|
|
18
18
|
import * as schema from './local-schema.ts'
|
|
19
|
-
import type { LocalDatabase } from './types.ts'
|
|
19
|
+
import type { LocalDatabase, WalletClient } from './types.ts'
|
|
20
20
|
|
|
21
21
|
export const configSchema = z.object({
|
|
22
22
|
privateKey: z.string().optional(),
|
|
@@ -118,13 +118,27 @@ export async function migrateLocalDatabase(db: LocalDatabase) {
|
|
|
118
118
|
cid text NOT NULL,
|
|
119
119
|
metadata text NOT NULL,
|
|
120
120
|
alternate_provider text NOT NULL,
|
|
121
|
-
|
|
121
|
+
tx_hash text,
|
|
122
122
|
error text,
|
|
123
123
|
created_at integer NOT NULL,
|
|
124
124
|
updated_at integer NOT NULL,
|
|
125
125
|
FOREIGN KEY (repair_id) REFERENCES repairs(id)
|
|
126
126
|
)
|
|
127
127
|
`)
|
|
128
|
+
try {
|
|
129
|
+
await db.$client.execute(`
|
|
130
|
+
ALTER TABLE operations ADD COLUMN tx_hash text
|
|
131
|
+
`)
|
|
132
|
+
} catch {
|
|
133
|
+
// Column already exists on databases created with the updated schema.
|
|
134
|
+
}
|
|
135
|
+
try {
|
|
136
|
+
await db.$client.execute(`
|
|
137
|
+
ALTER TABLE operations DROP COLUMN result
|
|
138
|
+
`)
|
|
139
|
+
} catch {
|
|
140
|
+
// Column does not exist on databases created with the updated schema.
|
|
141
|
+
}
|
|
128
142
|
}
|
|
129
143
|
|
|
130
144
|
/**
|
|
@@ -139,6 +153,62 @@ export function hashLink(hash: string, chain: Chain) {
|
|
|
139
153
|
return link
|
|
140
154
|
}
|
|
141
155
|
|
|
156
|
+
/**
|
|
157
|
+
* Mark operations with successful mined transactions as completed.
|
|
158
|
+
*
|
|
159
|
+
* @returns Operations that are not completed yet.
|
|
160
|
+
*/
|
|
161
|
+
export async function completeConfirmedOperations({
|
|
162
|
+
localDb,
|
|
163
|
+
client,
|
|
164
|
+
operations,
|
|
165
|
+
}: {
|
|
166
|
+
localDb: LocalDatabase
|
|
167
|
+
client: WalletClient
|
|
168
|
+
operations: OperationSelect[]
|
|
169
|
+
}): Promise<OperationSelect[]> {
|
|
170
|
+
const hashes = new Set(
|
|
171
|
+
operations.map((operation) => operation.txHash).filter((txHash): txHash is Hash => txHash != null)
|
|
172
|
+
)
|
|
173
|
+
const completedHashes = new Set<Hash>()
|
|
174
|
+
|
|
175
|
+
for (const hash of hashes) {
|
|
176
|
+
try {
|
|
177
|
+
const receipt = await getTransactionReceipt(client, { hash })
|
|
178
|
+
if (receipt.status === 'success') {
|
|
179
|
+
completedHashes.add(hash)
|
|
180
|
+
}
|
|
181
|
+
} catch (error) {
|
|
182
|
+
if (!(error instanceof TransactionReceiptNotFoundError)) {
|
|
183
|
+
throw error
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const completedOperations = operations.filter(
|
|
189
|
+
(operation) => operation.txHash != null && completedHashes.has(operation.txHash as Hash)
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
if (completedOperations.length > 0) {
|
|
193
|
+
await localDb
|
|
194
|
+
.update(schema.operations)
|
|
195
|
+
.set({
|
|
196
|
+
status: 'completed',
|
|
197
|
+
error: null,
|
|
198
|
+
updatedAt: Date.now(),
|
|
199
|
+
})
|
|
200
|
+
.where(
|
|
201
|
+
inArray(
|
|
202
|
+
schema.operations.id,
|
|
203
|
+
completedOperations.map((operation) => operation.id)
|
|
204
|
+
)
|
|
205
|
+
)
|
|
206
|
+
}
|
|
207
|
+
const completedOperationIds = new Set(completedOperations.map((operation) => operation.id))
|
|
208
|
+
|
|
209
|
+
return operations.filter((operation) => operation.status !== 'completed' && !completedOperationIds.has(operation.id))
|
|
210
|
+
}
|
|
211
|
+
|
|
142
212
|
/**
|
|
143
213
|
* Get a piece from a service URL
|
|
144
214
|
*/
|