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

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
+ }