@sentio/cli 3.5.1-rc.3 → 3.6.0-rc.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sentio/cli",
3
- "version": "3.5.1-rc.3",
3
+ "version": "3.6.0-rc.1",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "exports": {
@@ -337,7 +337,7 @@ async function runProcessorResume(processorId: string | undefined, options: Proc
337
337
  })
338
338
  }
339
339
 
340
- async function runProcessorStop(processorId: string | undefined, options: ProcessorOptions & { yes?: boolean }) {
340
+ export async function runProcessorStop(processorId: string | undefined, options: ProcessorOptions & { yes?: boolean }) {
341
341
  const resolvedProcessorId = await resolveAndConfirmProcessor('stop', processorId, options)
342
342
  if (!resolvedProcessorId) return
343
343
  const context = createApiContext(options)
@@ -0,0 +1,63 @@
1
+ import { Command } from '@commander-js/extra-typings'
2
+ import chalk from 'chalk'
3
+ import { handleCommandError } from '../api.js'
4
+ import { confirm } from './upload.js'
5
+ import { runProcessorStop } from './processor.js'
6
+ import {
7
+ getSentioNetworkConfig,
8
+ resolveNetworkAddresses,
9
+ getWalletFromPrivateKey,
10
+ requirePrivateKey,
11
+ stopProcessorOnChain
12
+ } from '../network.js'
13
+
14
+ export function createStopProcessorCommand() {
15
+ return new Command('stop')
16
+ .description('Stop a processor. Uses Sentio Network contract when --sentio-network is specified.')
17
+ .argument('<processorId>', 'ID of the processor to stop')
18
+ .option('--sentio-network <network>', 'Stop via Sentio Network contract (only "testnet" supported)')
19
+ .option('--host <host>', 'Override Sentio host')
20
+ .option('--api-key <key>', 'Use an explicit API key instead of saved credentials')
21
+ .option('--token <token>', 'Use an explicit bearer token instead of saved credentials')
22
+ .option('--project <owner/slug>', 'Sentio project in <owner>/<slug> format')
23
+ .option('--owner <owner>', 'Sentio project owner')
24
+ .option('--name <name>', 'Sentio project name')
25
+ .option('-y, --yes', 'Bypass confirmation')
26
+ .showHelpAfterError()
27
+ .action(async (processorId, options) => {
28
+ try {
29
+ if (options.sentioNetwork) {
30
+ await runStopProcessorOnChain(processorId, options)
31
+ } else {
32
+ await runProcessorStop(processorId, options)
33
+ }
34
+ } catch (error) {
35
+ handleCommandError(error)
36
+ }
37
+ })
38
+ }
39
+
40
+ async function runStopProcessorOnChain(processorId: string, options: { sentioNetwork?: string; yes?: boolean }) {
41
+ const network = options.sentioNetwork!
42
+ const networkConfig = getSentioNetworkConfig(network)
43
+
44
+ const privateKey = requirePrivateKey()
45
+ const wallet = getWalletFromPrivateKey(privateKey)
46
+
47
+ console.log(chalk.blue(`Wallet address: ${wallet.address}`))
48
+ console.log(chalk.blue('Resolving contract addresses from AddressBook...'))
49
+ const addresses = await resolveNetworkAddresses(networkConfig)
50
+
51
+ if (!options.yes) {
52
+ const confirmed = await confirm(`Stop processor "${processorId}" on Sentio Network ${network}?`)
53
+ if (!confirmed) {
54
+ console.log('Stop cancelled.')
55
+ return
56
+ }
57
+ }
58
+
59
+ await stopProcessorOnChain(networkConfig, addresses, wallet, processorId)
60
+
61
+ console.log()
62
+ console.log(chalk.green(`Processor "${processorId}" stopped on Sentio Network ${network}.`))
63
+ }
@@ -13,6 +13,21 @@ import readline from 'readline'
13
13
  import JSZip from 'jszip'
14
14
  import { UserInfo } from '../../../protos/lib/service/common/protos/common.js'
15
15
  import { CommandOptionsType } from './types.js'
16
+ import {
17
+ getSentioNetworkConfig,
18
+ resolveNetworkAddresses,
19
+ getWalletFromPrivateKey,
20
+ requirePrivateKey,
21
+ checkSTBalance,
22
+ checkBillingBalance,
23
+ uploadToIPFS,
24
+ createProcessorOnChain,
25
+ startProcessorOnChain,
26
+ stopProcessorOnChain,
27
+ confirmNoPlatformUpload,
28
+ getProcessorOnChain,
29
+ deleteProcessorOnChain
30
+ } from '../network.js'
16
31
  import { Auth, DefaultBatchUploader, FileType, IPFSBatchUploader, WalrusBatchUploader } from '../uploader.js'
17
32
  export { type Auth } from '../uploader.js'
18
33
 
@@ -56,16 +71,164 @@ export function createUploadCommand() {
56
71
  '--required-chain-id <chain_id...>',
57
72
  '(Optional) Specify chain IDs required for the Sentio network. This option is only available when --sentio-network is used. If omitted, all chain IDs from the project configuration (contracts or network overrides) will be used.'
58
73
  )
74
+ .option(
75
+ '--no-platform',
76
+ 'Upload processor directly to Sentio Network without platform support. Requires $PRIVATE_KEY env var. Only testnet is supported.'
77
+ )
78
+ .option(
79
+ '--ipfs-put-url <url>',
80
+ 'IPFS upload endpoint (used with --no-platform)',
81
+ 'https://api.sentio.xyz/v1/ipfs/add'
82
+ )
59
83
  .action(async (options, command) => {
60
84
  const processorConfig = loadProcessorConfig(options.path)
61
85
  overrideConfigWithOptions(processorConfig, options)
62
86
  if (options.path) {
63
87
  process.chdir(options.path)
64
88
  }
65
- await runUploadInternal(processorConfig, options, command)
89
+ if (options.platform === false) {
90
+ await runNoPlatformUpload(processorConfig, options)
91
+ } else {
92
+ await runUploadInternal(processorConfig, options, command)
93
+ }
66
94
  })
67
95
  }
68
96
 
97
+ async function runNoPlatformUpload(
98
+ processorConfig: YamlProjectConfig,
99
+ options: CommandOptionsType<typeof createUploadCommand>
100
+ ) {
101
+ // Determine network - default to testnet for --no-platform
102
+ const network = options.sentioNetwork || 'testnet'
103
+ const networkConfig = getSentioNetworkConfig(network)
104
+
105
+ // Step 1: Require PRIVATE_KEY
106
+ const privateKey = requirePrivateKey()
107
+ const wallet = getWalletFromPrivateKey(privateKey)
108
+ const walletAddress = wallet.address
109
+
110
+ // Step 2: Resolve contract addresses via AddressBook
111
+ console.log(chalk.blue('Resolving contract addresses from AddressBook...'))
112
+ const addresses = await resolveNetworkAddresses(networkConfig)
113
+
114
+ // Step 3: Check ST balance and Billing balance, then confirm
115
+ const [stBalance, billingBalance] = await Promise.all([
116
+ checkSTBalance(networkConfig, addresses, walletAddress),
117
+ checkBillingBalance(networkConfig, addresses, walletAddress)
118
+ ])
119
+ const confirmed = await confirmNoPlatformUpload(walletAddress, stBalance, billingBalance, addresses, networkConfig)
120
+ if (!confirmed) {
121
+ console.log('Upload cancelled.')
122
+ process.exit(0)
123
+ }
124
+
125
+ // Step 4: Build processor
126
+ if (!options.skipBuild) {
127
+ await buildProcessor(false, options)
128
+ }
129
+
130
+ const processorFile = path.join(process.cwd(), 'dist/lib.js')
131
+ if (!fs.existsSync(processorFile)) {
132
+ console.error(chalk.red('File not found:', processorFile, "- don't use --skip-build"))
133
+ process.exit(1)
134
+ }
135
+
136
+ const codeBuffer = fs.readFileSync(processorFile)
137
+ const stat = fs.statSync(processorFile)
138
+ console.log('Packed processor file size', Math.floor(stat.size / 1024) + 'K, last modified', stat.mtime)
139
+
140
+ // Step 5: Upload to IPFS
141
+ const ipfsPutUrl = options.ipfsPutUrl || 'https://api.sentio.xyz/v1/ipfs/add'
142
+ console.log(chalk.blue(`Uploading processor to IPFS (${ipfsPutUrl})...`))
143
+ const cid = await uploadToIPFS(codeBuffer, ipfsPutUrl)
144
+ console.log(chalk.green(`IPFS upload complete.`))
145
+ console.log('\t', chalk.blue('CID:'), cid)
146
+
147
+ // Step 6: Collect required chain IDs
148
+ let requiredChainIds = options.requiredChainId || processorConfig.requiredChainIds || []
149
+ if (requiredChainIds.length === 0) {
150
+ const chainIds = new Set<string>()
151
+ if (processorConfig.contracts) {
152
+ for (const contract of processorConfig.contracts) {
153
+ chainIds.add(String(contract.chain))
154
+ }
155
+ }
156
+ if (processorConfig.networkOverrides) {
157
+ for (const override of processorConfig.networkOverrides) {
158
+ chainIds.add(String(override.chain))
159
+ }
160
+ }
161
+ requiredChainIds = Array.from(chainIds)
162
+ }
163
+ if (requiredChainIds.length === 0) {
164
+ console.error(
165
+ chalk.red(
166
+ 'Error: No chain IDs found. Specify --required-chain-id or define contracts/networkOverrides in sentio.yaml.'
167
+ )
168
+ )
169
+ process.exit(1)
170
+ }
171
+
172
+ // Step 7: Derive processor ID from project name
173
+ let processorId = processorConfig.project.replace('/', '_')
174
+ const sdkVersion = getSdkVersion() || 'unknown'
175
+
176
+ // Step 7.5: Check if processor already exists on-chain
177
+ console.log(chalk.blue('Checking if processor already exists on-chain...'))
178
+ const existingProcessor = await getProcessorOnChain(networkConfig, addresses, processorId)
179
+
180
+ if (existingProcessor) {
181
+ const existingOwner = existingProcessor.owner.toLowerCase()
182
+ const currentWallet = wallet.address.toLowerCase()
183
+
184
+ if (existingOwner === currentWallet) {
185
+ // Same owner — prompt to replace
186
+ console.log(chalk.yellow(`Processor "${processorId}" already exists (owned by you).`))
187
+ const shouldReplace = await confirm(`Replace existing processor "${processorId}"?`)
188
+ if (!shouldReplace) {
189
+ console.log('Upload cancelled.')
190
+ process.exit(0)
191
+ }
192
+ // Stop and delete existing processor before re-creating
193
+ await stopProcessorOnChain(networkConfig, addresses, wallet, processorId)
194
+ await deleteProcessorOnChain(networkConfig, addresses, wallet, processorId)
195
+ } else {
196
+ // Different owner — prompt to rename
197
+ console.log(
198
+ chalk.yellow(
199
+ `Processor "${processorId}" already exists and is owned by ${existingProcessor.owner} (not your wallet ${wallet.address}).`
200
+ )
201
+ )
202
+ const randomSuffix = Math.random().toString(36).substring(2, 8)
203
+ const defaultNewId = `${processorId}-${randomSuffix}`
204
+
205
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
206
+ const newId: string = await new Promise((resolve) =>
207
+ rl.question(`Enter a new processor ID [${defaultNewId}]: `, (answer) => {
208
+ rl.close()
209
+ resolve(answer.trim() || defaultNewId)
210
+ })
211
+ )
212
+ processorId = newId
213
+ console.log(chalk.blue(`Using processor ID: ${processorId}`))
214
+ }
215
+ }
216
+
217
+ // Step 8: Create processor on-chain
218
+ await createProcessorOnChain(networkConfig, addresses, wallet, processorId, cid, requiredChainIds, sdkVersion)
219
+
220
+ // Step 9: Start processor on-chain
221
+ await startProcessorOnChain(networkConfig, addresses, wallet, processorId)
222
+
223
+ console.log()
224
+ console.log(chalk.green('=== Upload Complete ==='))
225
+ console.log('\t', chalk.blue('Processor ID:'), processorId)
226
+ console.log('\t', chalk.blue('IPFS CID:'), cid)
227
+ console.log('\t', chalk.blue('Network:'), network)
228
+ const codeHash = createHash('sha256').update(codeBuffer).digest('hex')
229
+ console.log('\t', chalk.blue('sha256:'), codeHash)
230
+ }
231
+
69
232
  function parseCheckpoints(
70
233
  checkpoints: string[] | undefined,
71
234
  continueFrom: number | undefined,
package/src/index.ts CHANGED
@@ -19,6 +19,7 @@ import { createPriceCommand } from './commands/price.js'
19
19
  import { createSimulationCommand } from './commands/simulation.js'
20
20
  import { createEndpointCommand } from './commands/endpoint.js'
21
21
  import { createDashboardCommand } from './commands/dashboard.js'
22
+ import { createStopProcessorCommand } from './commands/stop-processor.js'
22
23
  import { enableApiDebug } from './api.js'
23
24
  import { printVersions } from './utils.js'
24
25
 
@@ -52,5 +53,6 @@ program.addCommand(createPriceCommand())
52
53
  program.addCommand(createSimulationCommand())
53
54
  program.addCommand(createEndpointCommand())
54
55
  program.addCommand(createDashboardCommand())
56
+ program.addCommand(createStopProcessorCommand())
55
57
 
56
58
  program.parse()
package/src/network.ts ADDED
@@ -0,0 +1,407 @@
1
+ import { ethers } from 'ethers'
2
+ import chalk from 'chalk'
3
+ import fetch from 'node-fetch'
4
+ import readline from 'readline'
5
+
6
+ // --- Network Configuration ---
7
+
8
+ interface SentioNetworkConfig {
9
+ chainId: number
10
+ rpcUrl: string
11
+ explorerUrl: string
12
+ // AddressBook proxy that resolves all contract names to addresses
13
+ addressBookAddress: string
14
+ }
15
+
16
+ const TESTNET_CONFIG: SentioNetworkConfig = {
17
+ chainId: 7892101,
18
+ rpcUrl: 'https://testnet.sentio.xyz',
19
+ explorerUrl: 'https://testnet-explorer.sentio.xyz',
20
+ addressBookAddress: '0x17d5aF5Ed9C2558B802bEfcCc5a94C36dE95BB0B'
21
+ }
22
+
23
+ export function getSentioNetworkConfig(network: string): SentioNetworkConfig {
24
+ if (network === 'testnet' || network === '7892101') {
25
+ return TESTNET_CONFIG
26
+ }
27
+ if (network === 'mainnet' || network === '789210') {
28
+ console.error(chalk.red('Sentio Network mainnet is not yet supported. Only testnet is available.'))
29
+ process.exit(1)
30
+ }
31
+ console.error(chalk.red(`Invalid sentio network: ${network}. Only "testnet" is supported.`))
32
+ process.exit(1)
33
+ }
34
+
35
+ // --- Contract ABIs ---
36
+
37
+ // AddressBook: resolves contract names to addresses
38
+ const ADDRESS_BOOK_ABI = [
39
+ 'function getAddress(string name) view returns (address)',
40
+ 'function getAddress(bytes32 id) view returns (address)'
41
+ ]
42
+
43
+ // ProcessorRegistry: createProcessor, getProcessor, deleteProcessor
44
+ const PROCESSOR_REGISTRY_ABI = [
45
+ `function createProcessor(
46
+ string id,
47
+ tuple(uint8 sourceType, string ipfsCid) source,
48
+ tuple(string chainId, bool enableRpc, bool enableTrace)[] requireChains,
49
+ string sdkVersion
50
+ ) returns (string)`,
51
+ 'event ProcessorCreated(string indexed processorId)',
52
+ 'function getAllocations(string processorId) view returns (tuple(uint256 indexerId, uint256 timestamp, bool indexerReady)[])',
53
+ `function getProcessor(string processorId) view returns (
54
+ tuple(
55
+ string id,
56
+ bool active,
57
+ uint256 createdAt,
58
+ address owner,
59
+ string sdkVersion,
60
+ tuple(string chainId, bool enableRpc, bool enableTrace)[] requireChains,
61
+ tuple(uint8 sourceType, string ipfsCid) source,
62
+ tuple(uint256 indexerId, uint256 timestamp, bool indexerReady)[] allocations
63
+ )
64
+ )`,
65
+ 'function deleteProcessor(string processorId)'
66
+ ]
67
+
68
+ // Controller: startProcessor / stopProcessor
69
+ const CONTROLLER_ABI = ['function startProcessor(string processorId)', 'function stopProcessor(string processorId)']
70
+
71
+ // ERC20: balanceOf + approve
72
+ const ERC20_ABI = [
73
+ 'function balanceOf(address account) view returns (uint256)',
74
+ 'function approve(address spender, uint256 amount) returns (bool)'
75
+ ]
76
+
77
+ // Billing: deposit / depositTo / withdraw + balanceOf
78
+ const BILLING_ABI = ['function balances(address account) view returns (uint256)']
79
+
80
+ // --- Address Resolution via AddressBook ---
81
+
82
+ // Known address book key names for the contracts we need
83
+ const ADDRESS_BOOK_KEYS = {
84
+ processorRegistry: 'processor_registry',
85
+ controller: 'controller',
86
+ token: 'sentio_token',
87
+ billing: 'billing'
88
+ } as const
89
+
90
+ interface ResolvedAddresses {
91
+ addressBook: string
92
+ processorRegistry: string
93
+ controller: string
94
+ token: string
95
+ billing: string
96
+ }
97
+
98
+ let cachedAddresses: ResolvedAddresses | undefined
99
+
100
+ export async function resolveNetworkAddresses(config: SentioNetworkConfig): Promise<ResolvedAddresses> {
101
+ if (cachedAddresses) return cachedAddresses
102
+
103
+ const provider = new ethers.JsonRpcProvider(config.rpcUrl)
104
+ const addressBookAddr = config.addressBookAddress
105
+
106
+ console.log(chalk.gray(`AddressBook: ${addressBookAddr}`))
107
+
108
+ const addressBook = new ethers.Contract(addressBookAddr, ADDRESS_BOOK_ABI, provider)
109
+
110
+ const resolveAddress = async (name: string): Promise<string> => {
111
+ try {
112
+ // Try string-based lookup first
113
+ return await addressBook['getAddress(string)'](name)
114
+ } catch {
115
+ try {
116
+ // Fallback to bytes32-based lookup (keccak256 of name)
117
+ const id = ethers.id(name)
118
+ return await addressBook['getAddress(bytes32)'](id)
119
+ } catch (e) {
120
+ throw new Error(`Failed to resolve "${name}" from AddressBook (${addressBookAddr}): ${e.message}`)
121
+ }
122
+ }
123
+ }
124
+
125
+ const [processorRegistry, controller, token, billing] = await Promise.all([
126
+ resolveAddress(ADDRESS_BOOK_KEYS.processorRegistry),
127
+ resolveAddress(ADDRESS_BOOK_KEYS.controller),
128
+ resolveAddress(ADDRESS_BOOK_KEYS.token),
129
+ resolveAddress(ADDRESS_BOOK_KEYS.billing)
130
+ ])
131
+
132
+ console.log(chalk.gray(`ProcessorRegistry: ${processorRegistry}`))
133
+ console.log(chalk.gray(`Controller: ${controller}`))
134
+ console.log(chalk.gray(`ST Token: ${token}`))
135
+ console.log(chalk.gray(`Billing: ${billing}`))
136
+
137
+ cachedAddresses = { addressBook: addressBookAddr, processorRegistry, controller, token, billing }
138
+ return cachedAddresses
139
+ }
140
+
141
+ // --- Wallet Utilities ---
142
+
143
+ export function getWalletFromPrivateKey(privateKey: string): ethers.Wallet {
144
+ if (!privateKey.startsWith('0x')) {
145
+ privateKey = '0x' + privateKey
146
+ }
147
+ return new ethers.Wallet(privateKey)
148
+ }
149
+
150
+ export function requirePrivateKey(): string {
151
+ const pk = process.env.PRIVATE_KEY
152
+ if (!pk) {
153
+ console.error(
154
+ chalk.red('Error: $PRIVATE_KEY environment variable is required for Sentio Network direct transactions.')
155
+ )
156
+ console.error(chalk.red('Set it with: export PRIVATE_KEY=0x...'))
157
+ process.exit(1)
158
+ }
159
+ return pk
160
+ }
161
+
162
+ export async function checkSTBalance(
163
+ config: SentioNetworkConfig,
164
+ addresses: ResolvedAddresses,
165
+ walletAddress: string
166
+ ): Promise<bigint> {
167
+ const provider = new ethers.JsonRpcProvider(config.rpcUrl)
168
+ const token = new ethers.Contract(addresses.token, ERC20_ABI, provider)
169
+ const balance: bigint = await token.balanceOf(walletAddress)
170
+ return balance
171
+ }
172
+
173
+ export async function checkBillingBalance(
174
+ config: SentioNetworkConfig,
175
+ addresses: ResolvedAddresses,
176
+ walletAddress: string
177
+ ): Promise<bigint> {
178
+ const provider = new ethers.JsonRpcProvider(config.rpcUrl)
179
+ const billing = new ethers.Contract(addresses.billing, BILLING_ABI, provider)
180
+ try {
181
+ const balance: bigint = await billing.balances(walletAddress)
182
+ return balance
183
+ } catch {
184
+ // Billing contract may revert for accounts that have never deposited
185
+ return 0n
186
+ }
187
+ }
188
+
189
+ // --- IPFS Upload ---
190
+
191
+ export async function uploadToIPFS(fileBuffer: Buffer, ipfsUrl: string): Promise<string> {
192
+ const formData = new FormData()
193
+ const blob = new Blob([fileBuffer], { type: 'application/octet-stream' })
194
+ formData.append('file', blob, 'lib.js')
195
+
196
+ const response = await fetch(ipfsUrl, {
197
+ method: 'POST',
198
+ body: formData
199
+ })
200
+
201
+ if (!response.ok) {
202
+ const text = await response.text()
203
+ throw new Error(`IPFS upload failed: ${response.status} ${response.statusText} - ${text}`)
204
+ }
205
+
206
+ const result = (await response.json()) as { Hash: string }
207
+ return result.Hash
208
+ }
209
+
210
+ // --- Contract Interactions ---
211
+
212
+ export interface OnChainProcessor {
213
+ id: string
214
+ active: boolean
215
+ createdAt: bigint
216
+ owner: string
217
+ sdkVersion: string
218
+ }
219
+
220
+ /**
221
+ * Fetch processor info from on-chain registry. Returns null if the processor does not exist.
222
+ */
223
+ export async function getProcessorOnChain(
224
+ config: SentioNetworkConfig,
225
+ addresses: ResolvedAddresses,
226
+ processorId: string
227
+ ): Promise<OnChainProcessor | null> {
228
+ const provider = new ethers.JsonRpcProvider(config.rpcUrl)
229
+ const registry = new ethers.Contract(addresses.processorRegistry, PROCESSOR_REGISTRY_ABI, provider)
230
+
231
+ try {
232
+ const result = await registry.getProcessor(processorId)
233
+ // result is a tuple: (id, active, createdAt, owner, sdkVersion, requireChains, source, allocations)
234
+ const id: string = result[0]
235
+ if (!id || id === '') {
236
+ return null
237
+ }
238
+ return {
239
+ id,
240
+ active: result[1],
241
+ createdAt: result[2],
242
+ owner: result[3],
243
+ sdkVersion: result[4]
244
+ }
245
+ } catch {
246
+ // Contract reverts if processor doesn't exist
247
+ return null
248
+ }
249
+ }
250
+
251
+ export async function deleteProcessorOnChain(
252
+ config: SentioNetworkConfig,
253
+ addresses: ResolvedAddresses,
254
+ wallet: ethers.Wallet,
255
+ processorId: string
256
+ ): Promise<string> {
257
+ const provider = new ethers.JsonRpcProvider(config.rpcUrl)
258
+ const signer = wallet.connect(provider)
259
+
260
+ const registry = new ethers.Contract(addresses.processorRegistry, PROCESSOR_REGISTRY_ABI, signer)
261
+
262
+ console.log(chalk.blue('Deleting existing processor on-chain...'))
263
+ const tx = await registry.deleteProcessor(processorId)
264
+ console.log(chalk.gray(` Tx hash: ${tx.hash}`))
265
+ console.log(chalk.blue('Waiting for confirmation...'))
266
+
267
+ const receipt = await tx.wait()
268
+ if (receipt.status === 0) {
269
+ throw new Error(`deleteProcessor transaction failed. Tx: ${config.explorerUrl}/tx/${tx.hash}`)
270
+ }
271
+
272
+ console.log(chalk.green(`Processor deleted. Tx: ${config.explorerUrl}/tx/${tx.hash}`))
273
+ return tx.hash
274
+ }
275
+
276
+ export async function createProcessorOnChain(
277
+ config: SentioNetworkConfig,
278
+ addresses: ResolvedAddresses,
279
+ wallet: ethers.Wallet,
280
+ processorId: string,
281
+ ipfsCid: string,
282
+ requiredChainIds: string[],
283
+ sdkVersion: string
284
+ ): Promise<string> {
285
+ const provider = new ethers.JsonRpcProvider(config.rpcUrl)
286
+ const signer = wallet.connect(provider)
287
+
288
+ const registry = new ethers.Contract(addresses.processorRegistry, PROCESSOR_REGISTRY_ABI, signer)
289
+
290
+ const source = {
291
+ sourceType: 0, // IPFS
292
+ ipfsCid
293
+ }
294
+
295
+ const requireChains = requiredChainIds.map((chainId) => ({
296
+ chainId,
297
+ enableRpc: true,
298
+ enableTrace: false
299
+ }))
300
+
301
+ console.log(chalk.blue('Creating processor on-chain...'))
302
+ console.log(chalk.gray(` Processor ID: ${processorId}`))
303
+ console.log(chalk.gray(` IPFS CID: ${ipfsCid}`))
304
+ console.log(chalk.gray(` Chains: ${requiredChainIds.join(', ')}`))
305
+ console.log(chalk.gray(` SDK Version: ${sdkVersion}`))
306
+
307
+ const tx = await registry.createProcessor(processorId, source, requireChains, sdkVersion)
308
+ console.log(chalk.gray(` Tx hash: ${tx.hash}`))
309
+ console.log(chalk.blue('Waiting for confirmation...'))
310
+
311
+ const receipt = await tx.wait()
312
+ if (receipt.status === 0) {
313
+ throw new Error(`createProcessor transaction failed. Tx: ${config.explorerUrl}/tx/${tx.hash}`)
314
+ }
315
+
316
+ console.log(chalk.green(`Processor created. Tx: ${config.explorerUrl}/tx/${tx.hash}`))
317
+ return tx.hash
318
+ }
319
+
320
+ export async function startProcessorOnChain(
321
+ config: SentioNetworkConfig,
322
+ addresses: ResolvedAddresses,
323
+ wallet: ethers.Wallet,
324
+ processorId: string
325
+ ): Promise<string> {
326
+ const provider = new ethers.JsonRpcProvider(config.rpcUrl)
327
+ const signer = wallet.connect(provider)
328
+
329
+ const controller = new ethers.Contract(addresses.controller, CONTROLLER_ABI, signer)
330
+
331
+ console.log(chalk.blue('Starting processor on-chain...'))
332
+ const tx = await controller.startProcessor(processorId)
333
+ console.log(chalk.gray(` Tx hash: ${tx.hash}`))
334
+ console.log(chalk.blue('Waiting for confirmation...'))
335
+
336
+ const receipt = await tx.wait()
337
+ if (receipt.status === 0) {
338
+ throw new Error(`startProcessor transaction failed. Tx: ${config.explorerUrl}/tx/${tx.hash}`)
339
+ }
340
+
341
+ console.log(chalk.green(`Processor started. Tx: ${config.explorerUrl}/tx/${tx.hash}`))
342
+ return tx.hash
343
+ }
344
+
345
+ export async function stopProcessorOnChain(
346
+ config: SentioNetworkConfig,
347
+ addresses: ResolvedAddresses,
348
+ wallet: ethers.Wallet,
349
+ processorId: string
350
+ ): Promise<string> {
351
+ const provider = new ethers.JsonRpcProvider(config.rpcUrl)
352
+ const signer = wallet.connect(provider)
353
+
354
+ const controller = new ethers.Contract(addresses.controller, CONTROLLER_ABI, signer)
355
+
356
+ console.log(chalk.blue('Stopping processor on-chain...'))
357
+ const tx = await controller.stopProcessor(processorId)
358
+ console.log(chalk.gray(` Tx hash: ${tx.hash}`))
359
+ console.log(chalk.blue('Waiting for confirmation...'))
360
+
361
+ const receipt = await tx.wait()
362
+ if (receipt.status === 0) {
363
+ throw new Error(`stopProcessor transaction failed. Tx: ${config.explorerUrl}/tx/${tx.hash}`)
364
+ }
365
+
366
+ console.log(chalk.green(`Processor stopped. Tx: ${config.explorerUrl}/tx/${tx.hash}`))
367
+ return tx.hash
368
+ }
369
+
370
+ // --- Confirmation Prompt ---
371
+
372
+ export async function confirmNoPlatformUpload(
373
+ walletAddress: string,
374
+ stBalance: bigint,
375
+ billingBalance: bigint,
376
+ addresses: ResolvedAddresses,
377
+ networkConfig: SentioNetworkConfig
378
+ ): Promise<boolean> {
379
+ const formattedST = ethers.formatEther(stBalance)
380
+ const formattedBilling = ethers.formatEther(billingBalance)
381
+ console.log()
382
+ console.log(chalk.blue('=== Sentio Network Direct Upload (No Platform) ==='))
383
+ console.log(chalk.white(` Address: ${walletAddress}`))
384
+ console.log(chalk.white(` ST Balance: ${formattedST} ST`))
385
+ console.log(chalk.white(` Billing Balance: ${formattedBilling} ST`))
386
+
387
+ if (billingBalance === 0n) {
388
+ console.log()
389
+ console.log(
390
+ chalk.yellow(
391
+ ' ⚠ Your Billing balance is 0. Indexing fees are charged from the Billing contract.\n' +
392
+ ' You must deposit ST tokens into the Billing contract before your processor can run.\n' +
393
+ ` Use: cast send ${addresses.token} "approve(address,uint256)" ${addresses.billing} <amount> --rpc-url ${networkConfig.rpcUrl} --private-key $PRIVATE_KEY\n` +
394
+ ` cast send ${addresses.billing} "deposit(uint256)" <amount> --rpc-url ${networkConfig.rpcUrl} --private-key $PRIVATE_KEY`
395
+ )
396
+ )
397
+ }
398
+ console.log()
399
+
400
+ const rl = readline.createInterface({
401
+ input: process.stdin,
402
+ output: process.stdout
403
+ })
404
+ const answer: string = await new Promise((resolve) => rl.question('Continue with upload? (yes/no) ', resolve))
405
+ rl.close()
406
+ return ['y', 'yes'].includes(answer.toLowerCase())
407
+ }