@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.
Files changed (64) hide show
  1. package/dist/package.json +1 -1
  2. package/dist/src/cli.js +2 -0
  3. package/dist/src/cli.js.map +1 -1
  4. package/dist/src/commands/datasets.d.ts +1 -0
  5. package/dist/src/commands/datasets.d.ts.map +1 -1
  6. package/dist/src/commands/datasets.js +5 -5
  7. package/dist/src/commands/datasets.js.map +1 -1
  8. package/dist/src/commands/providers.d.ts +1 -0
  9. package/dist/src/commands/providers.d.ts.map +1 -1
  10. package/dist/src/commands/repair.d.ts +1 -0
  11. package/dist/src/commands/repair.d.ts.map +1 -1
  12. package/dist/src/commands/repair.js +4 -0
  13. package/dist/src/commands/repair.js.map +1 -1
  14. package/dist/src/commands/replicate.d.ts +1 -0
  15. package/dist/src/commands/replicate.d.ts.map +1 -1
  16. package/dist/src/commands/replicate.js +4 -0
  17. package/dist/src/commands/replicate.js.map +1 -1
  18. package/dist/src/commands/session-key.d.ts +25 -0
  19. package/dist/src/commands/session-key.d.ts.map +1 -0
  20. package/dist/src/commands/session-key.js +114 -0
  21. package/dist/src/commands/session-key.js.map +1 -0
  22. package/dist/src/commands/wallet.d.ts +1 -0
  23. package/dist/src/commands/wallet.d.ts.map +1 -1
  24. package/dist/src/commands/wallet.js +75 -26
  25. package/dist/src/commands/wallet.js.map +1 -1
  26. package/dist/src/db/dedupe-cids.d.ts +1 -1
  27. package/dist/src/db/update-operation.d.ts +2 -2
  28. package/dist/src/db/update-operation.d.ts.map +1 -1
  29. package/dist/src/db/update-operation.js +2 -2
  30. package/dist/src/db/upsert-operations.js +1 -1
  31. package/dist/src/local-schema.d.ts +8 -11
  32. package/dist/src/local-schema.d.ts.map +1 -1
  33. package/dist/src/local-schema.js +1 -1
  34. package/dist/src/local-schema.js.map +1 -1
  35. package/dist/src/middleware.d.ts +2 -0
  36. package/dist/src/middleware.d.ts.map +1 -1
  37. package/dist/src/middleware.js +2 -0
  38. package/dist/src/middleware.js.map +1 -1
  39. package/dist/src/pipeline/add-pieces.d.ts.map +1 -1
  40. package/dist/src/pipeline/add-pieces.js +17 -3
  41. package/dist/src/pipeline/add-pieces.js.map +1 -1
  42. package/dist/src/pipeline/create-datasets.d.ts +4 -2
  43. package/dist/src/pipeline/create-datasets.d.ts.map +1 -1
  44. package/dist/src/pipeline/create-datasets.js +5 -5
  45. package/dist/src/pipeline/create-datasets.js.map +1 -1
  46. package/dist/src/utils.d.ts +34 -30
  47. package/dist/src/utils.d.ts.map +1 -1
  48. package/dist/src/utils.js +48 -3
  49. package/dist/src/utils.js.map +1 -1
  50. package/package.json +1 -1
  51. package/readme.md +40 -3
  52. package/src/cli.ts +2 -0
  53. package/src/commands/datasets.ts +7 -5
  54. package/src/commands/repair.ts +4 -0
  55. package/src/commands/replicate.ts +4 -0
  56. package/src/commands/session-key.ts +121 -0
  57. package/src/commands/wallet.ts +75 -26
  58. package/src/db/update-operation.ts +3 -3
  59. package/src/db/upsert-operations.ts +1 -1
  60. package/src/local-schema.ts +1 -7
  61. package/src/middleware.ts +2 -0
  62. package/src/pipeline/add-pieces.ts +19 -3
  63. package/src/pipeline/create-datasets.ts +20 -5
  64. 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
+ })
@@ -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
- async *run(c) {
21
- const { client, chain } = c.var
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
- yield 'Funding wallet...'
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
- yield `Waiting for tx ${hashLink(hashes[0].tx_hash, chain)} to be mined...`
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
- yield {
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: 'Failed to fund wallet',
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: client.account.address,
83
+ address,
65
84
  })
66
85
 
67
86
  const balanceUSDFC = await ERC20.balance(client, {
68
- address: client.account.address,
87
+ address,
69
88
  })
70
89
 
71
90
  const summary = await Pay.getAccountSummary(client, {
72
- address: client.account.address,
91
+ address,
73
92
  })
74
93
  return {
75
- address: client.account.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
- async *run(c) {
103
- const { client, chain } = c.var
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
- yield `Depositing ${c.args.amount} tokens to wallet...`
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
- yield `Waiting for tx ${hashLink(hash, chain)} to be mined...`
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
- yield `Deposit successful`
115
- return
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: (error as Error).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
- async *run(c) {
136
- const { client, chain } = c.var
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
- yield `Withdrawing ${c.args.amount} USDFC from pay account...`
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
- yield `Waiting for tx ${hashLink(hash, chain)} to be mined...`
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
- yield `Withdrawal successful`
148
- return
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: (error as Error).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
- result?: localSchema.OperationResult | null
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, result, error }: UpdateOperationOptions) {
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
- result,
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', 'result']),
21
+ set: buildConflictUpdateColumns(localSchema.operations, ['status', 'error', 'updatedAt', 'txHash']),
22
22
  })
23
23
  }
@@ -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
- result: jsonType().$type<OperationResult>(),
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 addPiecesResult2 = await SP.waitForAddPieces(addPiecesResult)
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
- result: { dataSetId: addPiecesResult2.dataSetId, txHash: addPiecesResult2.txHash },
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({ source, localDb, indexerDb, client, repair }: EnsureRepairDatasetOptions) {
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: client.account.address,
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: client.account.address,
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({ localDb, indexerDb, client, repair }: EnsureRepairDatasetOptions) {
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: client.account.address,
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
- result text,
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
  */