@sentio/cli 3.5.1-rc.2 → 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/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
+ }
package/src/uploader.ts CHANGED
@@ -89,7 +89,8 @@ export abstract class BatchUploader {
89
89
  debug?: boolean,
90
90
  continueFrom?: number,
91
91
  networkOverrides?: NetworkOverride[],
92
- rollback?: Record<string, number>
92
+ rollback?: Record<string, number>,
93
+ initResponse?: InitBatchUploadResponse
93
94
  ): Promise<FinishBatchUploadResponse>
94
95
 
95
96
  async initUpload(fileTypes?: Record<string, FileType>): Promise<InitBatchUploadResponse> {
@@ -184,16 +185,18 @@ export class DefaultBatchUploader extends BatchUploader {
184
185
  debug?: boolean,
185
186
  continueFrom?: number,
186
187
  networkOverrides?: NetworkOverride[],
187
- rollback?: Record<string, number>
188
+ rollback?: Record<string, number>,
189
+ initResponse?: InitBatchUploadResponse
188
190
  ): Promise<FinishBatchUploadResponse> {
189
- // Step 1: Initialize upload with file types
190
- const fileTypes: Record<string, FileType> = {
191
- source: FileType.SOURCE,
192
- code: FileType.PROCESSOR
191
+ // Step 1: Initialize upload with file types (if not already done)
192
+ if (!initResponse) {
193
+ const fileTypes: Record<string, FileType> = {
194
+ source: FileType.SOURCE,
195
+ code: FileType.PROCESSOR
196
+ }
197
+ initResponse = await this.initUpload(fileTypes)
193
198
  }
194
199
 
195
- const initResponse = await this.initUpload(fileTypes)
196
-
197
200
  // Step 3: Upload files to S3 using presigned URLs
198
201
  for (const [fileKey, payload] of Object.entries(initResponse.payloads)) {
199
202
  if (!payload?.object?.putUrl) {
@@ -235,7 +238,7 @@ export class DefaultBatchUploader extends BatchUploader {
235
238
 
236
239
  export class IPFSBatchUploader extends BatchUploader {
237
240
  constructor(options: YamlProjectConfig, auth: Auth) {
238
- super(StorageEngine.DEFAULT, options, auth)
241
+ super(StorageEngine.IPFS, options, auth)
239
242
  }
240
243
 
241
244
  async upload(
@@ -245,15 +248,17 @@ export class IPFSBatchUploader extends BatchUploader {
245
248
  debug?: boolean,
246
249
  continueFrom?: number,
247
250
  networkOverrides?: NetworkOverride[],
248
- rollback?: Record<string, number>
251
+ rollback?: Record<string, number>,
252
+ initResponse?: InitBatchUploadResponse
249
253
  ): Promise<FinishBatchUploadResponse> {
250
- const fileTypes: Record<string, FileType> = {
251
- source: FileType.SOURCE,
252
- code: FileType.PROCESSOR
254
+ if (!initResponse) {
255
+ const fileTypes: Record<string, FileType> = {
256
+ source: FileType.SOURCE,
257
+ code: FileType.PROCESSOR
258
+ }
259
+ initResponse = await this.initUpload(fileTypes)
253
260
  }
254
261
 
255
- const initResponse = await this.initUpload(fileTypes)
256
-
257
262
  for (const [fileKey, payload] of Object.entries(initResponse.payloads)) {
258
263
  const putUrl = payload.object?.putUrl || payload.ipfs?.putUrl
259
264
  if (!putUrl) {
@@ -334,16 +339,18 @@ export class WalrusBatchUploader extends BatchUploader {
334
339
  debug?: boolean,
335
340
  continueFrom?: number,
336
341
  networkOverrides?: NetworkOverride[],
337
- rollback?: Record<string, number>
342
+ rollback?: Record<string, number>,
343
+ initResponse?: InitBatchUploadResponse
338
344
  ): Promise<FinishBatchUploadResponse> {
339
- // Step 1: Initialize upload with file types
340
- const fileTypes: Record<string, FileType> = {
341
- source: FileType.SOURCE,
342
- code: FileType.PROCESSOR
345
+ // Step 1: Initialize upload with file types (if not already done)
346
+ if (!initResponse) {
347
+ const fileTypes: Record<string, FileType> = {
348
+ source: FileType.SOURCE,
349
+ code: FileType.PROCESSOR
350
+ }
351
+ initResponse = await this.initUpload(fileTypes)
343
352
  }
344
353
 
345
- const initResponse = await this.initUpload(fileTypes)
346
-
347
354
  // Step 2: Create quilt multipart form data for all files
348
355
  const formData = this.createQuiltMultipartFormData(files)
349
356