@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/lib/index.js +38418 -11002
- package/package.json +1 -1
- package/src/commands/dashboard.ts +86 -0
- package/src/commands/processor.ts +1 -1
- package/src/commands/stop-processor.ts +63 -0
- package/src/commands/upload.ts +164 -1
- package/src/index.ts +2 -0
- package/src/network.ts +407 -0
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
|
+
}
|