@sentio/cli 3.6.2 → 3.7.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/lib/index.js CHANGED
@@ -139214,8 +139214,14 @@ var PROCESSOR_REGISTRY_ABI = [
139214
139214
  tuple(string chainId, bool enableRpc, bool enableTrace)[] requireChains,
139215
139215
  string sdkVersion
139216
139216
  ) returns (string)`,
139217
+ `function createProcessor(
139218
+ address owner,
139219
+ string id,
139220
+ tuple(uint8 sourceType, string ipfsCid) source,
139221
+ tuple(string chainId, bool enableRpc, bool enableTrace)[] requireChains,
139222
+ string sdkVersion
139223
+ ) returns (string)`,
139217
139224
  "event ProcessorCreated(string indexed processorId)",
139218
- "function getAllocations(string processorId) view returns (tuple(uint256 indexerId, uint256 timestamp, bool indexerReady)[])",
139219
139225
  `function getProcessor(string processorId) view returns (
139220
139226
  tuple(
139221
139227
  string id,
@@ -139225,12 +139231,18 @@ var PROCESSOR_REGISTRY_ABI = [
139225
139231
  string sdkVersion,
139226
139232
  tuple(string chainId, bool enableRpc, bool enableTrace)[] requireChains,
139227
139233
  tuple(uint8 sourceType, string ipfsCid) source,
139228
- tuple(uint256 indexerId, uint256 timestamp, bool indexerReady)[] allocations
139234
+ tuple(uint256 indexerId, uint256 allocationTime, bool indexerReady, uint256 replicaIndex)[] allocations
139229
139235
  )
139230
139236
  )`,
139231
139237
  "function deleteProcessor(string processorId)"
139232
139238
  ];
139233
- var CONTROLLER_ABI = ["function startProcessor(string processorId)", "function stopProcessor(string processorId)"];
139239
+ var CONTROLLER_ABI = [
139240
+ "function startProcessor(string processorId)",
139241
+ "function stopProcessor(string processorId)",
139242
+ "function getAllocations(string processorId) view returns (tuple(uint256 indexerId, uint256 allocationTime, bool indexerReady, uint256 replicaIndex)[])"
139243
+ ];
139244
+ var DATABASES_ABI = ["function getProcessorDatabases(string processorId) view returns (string[])"];
139245
+ var PERMISSIONS_ABI = ["function isOperator(address account, address operator) view returns (bool)"];
139234
139246
  var ERC20_ABI = [
139235
139247
  "function balanceOf(address account) view returns (uint256)",
139236
139248
  "function approve(address spender, uint256 amount) returns (bool)"
@@ -139240,7 +139252,9 @@ var ADDRESS_BOOK_KEYS = {
139240
139252
  processorRegistry: "processor_registry",
139241
139253
  controller: "controller",
139242
139254
  token: "sentio_token",
139243
- billing: "billing"
139255
+ billing: "billing",
139256
+ databases: "databases",
139257
+ permissions: "permissions"
139244
139258
  };
139245
139259
  var cachedAddresses;
139246
139260
  async function resolveNetworkAddresses(config) {
@@ -139261,17 +139275,29 @@ async function resolveNetworkAddresses(config) {
139261
139275
  }
139262
139276
  }
139263
139277
  };
139264
- const [processorRegistry, controller, token, billing] = await Promise.all([
139278
+ const [processorRegistry, controller, token, billing, databases, permissions] = await Promise.all([
139265
139279
  resolveAddress2(ADDRESS_BOOK_KEYS.processorRegistry),
139266
139280
  resolveAddress2(ADDRESS_BOOK_KEYS.controller),
139267
139281
  resolveAddress2(ADDRESS_BOOK_KEYS.token),
139268
- resolveAddress2(ADDRESS_BOOK_KEYS.billing)
139282
+ resolveAddress2(ADDRESS_BOOK_KEYS.billing),
139283
+ resolveAddress2(ADDRESS_BOOK_KEYS.databases),
139284
+ resolveAddress2(ADDRESS_BOOK_KEYS.permissions)
139269
139285
  ]);
139270
139286
  console.log(source_default.gray(`ProcessorRegistry: ${processorRegistry}`));
139271
139287
  console.log(source_default.gray(`Controller: ${controller}`));
139272
139288
  console.log(source_default.gray(`ST Token: ${token}`));
139273
139289
  console.log(source_default.gray(`Billing: ${billing}`));
139274
- cachedAddresses = { addressBook: addressBookAddr, processorRegistry, controller, token, billing };
139290
+ console.log(source_default.gray(`Databases: ${databases}`));
139291
+ console.log(source_default.gray(`Permissions: ${permissions}`));
139292
+ cachedAddresses = {
139293
+ addressBook: addressBookAddr,
139294
+ processorRegistry,
139295
+ controller,
139296
+ token,
139297
+ billing,
139298
+ databases,
139299
+ permissions
139300
+ };
139275
139301
  return cachedAddresses;
139276
139302
  }
139277
139303
  function getWalletFromPrivateKey(privateKey) {
@@ -139307,6 +139333,11 @@ async function checkBillingBalance(config, addresses, walletAddress) {
139307
139333
  return 0n;
139308
139334
  }
139309
139335
  }
139336
+ async function isOperatorOnChain(config, addresses, owner, operator) {
139337
+ const provider = new ethers_exports.JsonRpcProvider(config.rpcUrl);
139338
+ const permissions = new ethers_exports.Contract(addresses.permissions, PERMISSIONS_ABI, provider);
139339
+ return await permissions.isOperator(owner, operator);
139340
+ }
139310
139341
  async function uploadToIPFS(fileBuffer, ipfsUrl) {
139311
139342
  const formData = new FormData();
139312
139343
  const blob = new Blob([fileBuffer], { type: "application/octet-stream" });
@@ -139322,6 +139353,26 @@ async function uploadToIPFS(fileBuffer, ipfsUrl) {
139322
139353
  const result = await response.json();
139323
139354
  return result.Hash;
139324
139355
  }
139356
+ async function submitAndWait(explorerUrl, label, txPromise) {
139357
+ let tx;
139358
+ try {
139359
+ tx = await txPromise;
139360
+ } catch (err) {
139361
+ throw new Error(`${label} failed to submit: ${err.shortMessage ?? err.message}`);
139362
+ }
139363
+ console.log(source_default.gray(` Tx hash: ${explorerUrl}/tx/${tx.hash}`));
139364
+ console.log(source_default.blue("Waiting for confirmation..."));
139365
+ let receipt;
139366
+ try {
139367
+ receipt = await tx.wait();
139368
+ } catch (err) {
139369
+ throw new Error(`${label} failed: ${err.shortMessage ?? err.message}. Tx: ${explorerUrl}/tx/${tx.hash}`);
139370
+ }
139371
+ if (!receipt || receipt.status === 0) {
139372
+ throw new Error(`${label} transaction reverted. Tx: ${explorerUrl}/tx/${tx.hash}`);
139373
+ }
139374
+ return tx;
139375
+ }
139325
139376
  async function getProcessorOnChain(config, addresses, processorId) {
139326
139377
  const provider = new ethers_exports.JsonRpcProvider(config.rpcUrl);
139327
139378
  const registry = new ethers_exports.Contract(addresses.processorRegistry, PROCESSOR_REGISTRY_ABI, provider);
@@ -139347,17 +139398,31 @@ async function deleteProcessorOnChain(config, addresses, wallet, processorId) {
139347
139398
  const signer = wallet.connect(provider);
139348
139399
  const registry = new ethers_exports.Contract(addresses.processorRegistry, PROCESSOR_REGISTRY_ABI, signer);
139349
139400
  console.log(source_default.blue("Deleting existing processor on-chain..."));
139350
- const tx = await registry.deleteProcessor(processorId);
139351
- console.log(source_default.gray(` Tx hash: ${tx.hash}`));
139352
- console.log(source_default.blue("Waiting for confirmation..."));
139353
- const receipt = await tx.wait();
139354
- if (receipt.status === 0) {
139355
- throw new Error(`deleteProcessor transaction failed. Tx: ${config.explorerUrl}/tx/${tx.hash}`);
139356
- }
139401
+ const tx = await submitAndWait(config.explorerUrl, "deleteProcessor", registry.deleteProcessor(processorId));
139357
139402
  console.log(source_default.green(`Processor deleted. Tx: ${config.explorerUrl}/tx/${tx.hash}`));
139403
+ await waitForProcessorDatabaseCleanup(addresses, processorId, provider);
139358
139404
  return tx.hash;
139359
139405
  }
139360
- async function createProcessorOnChain(config, addresses, wallet, processorId, ipfsCid, requiredChainIds, sdkVersion) {
139406
+ async function waitForProcessorDatabaseCleanup(addresses, processorId, provider, timeoutMs = 12e4, pollIntervalMs = 5e3) {
139407
+ const databases = new ethers_exports.Contract(addresses.databases, DATABASES_ABI, provider);
139408
+ const dbIds = await databases.getProcessorDatabases(processorId);
139409
+ if (dbIds.length === 0) {
139410
+ return;
139411
+ }
139412
+ console.log(source_default.blue(`Waiting for ${dbIds.length} replica database(s) to be cleaned up...`));
139413
+ const deadline = Date.now() + timeoutMs;
139414
+ while (Date.now() < deadline) {
139415
+ await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
139416
+ const remaining = await databases.getProcessorDatabases(processorId);
139417
+ if (remaining.length === 0) {
139418
+ console.log(source_default.green("All replica databases cleaned up."));
139419
+ return;
139420
+ }
139421
+ console.log(source_default.gray(` ${remaining.length} replica database(s) still pending cleanup...`));
139422
+ }
139423
+ throw new Error(`Timeout: processor databases not cleaned up within ${timeoutMs / 1e3}s`);
139424
+ }
139425
+ async function createProcessorOnChain(config, addresses, wallet, processorId, ipfsCid, requiredChainIds, sdkVersion, ownerOverride) {
139361
139426
  const provider = new ethers_exports.JsonRpcProvider(config.rpcUrl);
139362
139427
  const signer = wallet.connect(provider);
139363
139428
  const registry = new ethers_exports.Contract(addresses.processorRegistry, PROCESSOR_REGISTRY_ABI, signer);
@@ -139376,13 +139441,22 @@ async function createProcessorOnChain(config, addresses, wallet, processorId, ip
139376
139441
  console.log(source_default.gray(` IPFS CID: ${ipfsCid}`));
139377
139442
  console.log(source_default.gray(` Chains: ${requiredChainIds.join(", ")}`));
139378
139443
  console.log(source_default.gray(` SDK Version: ${sdkVersion}`));
139379
- const tx = await registry.createProcessor(processorId, source, requireChains, sdkVersion);
139380
- console.log(source_default.gray(` Tx hash: ${tx.hash}`));
139381
- console.log(source_default.blue("Waiting for confirmation..."));
139382
- const receipt = await tx.wait();
139383
- if (receipt.status === 0) {
139384
- throw new Error(`createProcessor transaction failed. Tx: ${config.explorerUrl}/tx/${tx.hash}`);
139385
- }
139444
+ if (ownerOverride) {
139445
+ console.log(source_default.gray(` Owner: ${ownerOverride} (operator: ${wallet.address})`));
139446
+ }
139447
+ const txPromise = ownerOverride ? registry["createProcessor(address,string,(uint8,string),(string,bool,bool)[],string)"](
139448
+ ownerOverride,
139449
+ processorId,
139450
+ source,
139451
+ requireChains,
139452
+ sdkVersion
139453
+ ) : registry["createProcessor(string,(uint8,string),(string,bool,bool)[],string)"](
139454
+ processorId,
139455
+ source,
139456
+ requireChains,
139457
+ sdkVersion
139458
+ );
139459
+ const tx = await submitAndWait(config.explorerUrl, "createProcessor", txPromise);
139386
139460
  console.log(source_default.green(`Processor created. Tx: ${config.explorerUrl}/tx/${tx.hash}`));
139387
139461
  return tx.hash;
139388
139462
  }
@@ -139391,13 +139465,7 @@ async function startProcessorOnChain(config, addresses, wallet, processorId) {
139391
139465
  const signer = wallet.connect(provider);
139392
139466
  const controller = new ethers_exports.Contract(addresses.controller, CONTROLLER_ABI, signer);
139393
139467
  console.log(source_default.blue("Starting processor on-chain..."));
139394
- const tx = await controller.startProcessor(processorId);
139395
- console.log(source_default.gray(` Tx hash: ${tx.hash}`));
139396
- console.log(source_default.blue("Waiting for confirmation..."));
139397
- const receipt = await tx.wait();
139398
- if (receipt.status === 0) {
139399
- throw new Error(`startProcessor transaction failed. Tx: ${config.explorerUrl}/tx/${tx.hash}`);
139400
- }
139468
+ const tx = await submitAndWait(config.explorerUrl, "startProcessor", controller.startProcessor(processorId));
139401
139469
  console.log(source_default.green(`Processor started. Tx: ${config.explorerUrl}/tx/${tx.hash}`));
139402
139470
  return tx.hash;
139403
139471
  }
@@ -139405,33 +139473,41 @@ async function stopProcessorOnChain(config, addresses, wallet, processorId) {
139405
139473
  const provider = new ethers_exports.JsonRpcProvider(config.rpcUrl);
139406
139474
  const signer = wallet.connect(provider);
139407
139475
  const controller = new ethers_exports.Contract(addresses.controller, CONTROLLER_ABI, signer);
139408
- console.log(source_default.blue("Stopping processor on-chain..."));
139409
- const tx = await controller.stopProcessor(processorId);
139410
- console.log(source_default.gray(` Tx hash: ${tx.hash}`));
139411
- console.log(source_default.blue("Waiting for confirmation..."));
139412
- const receipt = await tx.wait();
139413
- if (receipt.status === 0) {
139414
- throw new Error(`stopProcessor transaction failed. Tx: ${config.explorerUrl}/tx/${tx.hash}`);
139476
+ const allocations = await controller.getAllocations(processorId);
139477
+ if (allocations.length === 0) {
139478
+ console.log(source_default.gray("Processor has no allocations, skipping stopProcessor."));
139479
+ return "";
139415
139480
  }
139481
+ console.log(source_default.blue("Stopping processor on-chain..."));
139482
+ const tx = await submitAndWait(config.explorerUrl, "stopProcessor", controller.stopProcessor(processorId));
139416
139483
  console.log(source_default.green(`Processor stopped. Tx: ${config.explorerUrl}/tx/${tx.hash}`));
139417
139484
  return tx.hash;
139418
139485
  }
139419
- async function confirmNoPlatformUpload(walletAddress, stBalance, billingBalance, addresses, networkConfig) {
139486
+ async function confirmNoPlatformUpload(walletAddress, stBalance, billingBalance, addresses, networkConfig, ownerOverride) {
139420
139487
  const formattedST = ethers_exports.formatEther(stBalance);
139421
139488
  const formattedBilling = ethers_exports.formatEther(billingBalance);
139422
139489
  console.log();
139423
139490
  console.log(source_default.blue("=== Sentio Network Direct Upload (No Platform) ==="));
139424
- console.log(source_default.white(` Address: ${walletAddress}`));
139491
+ if (ownerOverride) {
139492
+ console.log(source_default.white(` Owner: ${ownerOverride}`));
139493
+ console.log(source_default.white(` Operator: ${walletAddress}`));
139494
+ } else {
139495
+ console.log(source_default.white(` Address: ${walletAddress}`));
139496
+ }
139425
139497
  console.log(source_default.white(` ST Balance: ${formattedST} ST`));
139426
139498
  console.log(source_default.white(` Billing Balance: ${formattedBilling} ST`));
139427
139499
  if (billingBalance === 0n) {
139500
+ const who = ownerOverride ? `the owner (${ownerOverride})` : "you";
139501
+ const keyHint = ownerOverride ? "<owner's private key>" : "$PRIVATE_KEY";
139428
139502
  console.log();
139429
139503
  console.log(
139430
139504
  source_default.yellow(
139431
- ` \u26A0 Your Billing balance is 0. Indexing fees are charged from the Billing contract.
139432
- You must deposit ST tokens into the Billing contract before your processor can run.
139433
- Use: cast send ${addresses.token} "approve(address,uint256)" ${addresses.billing} <amount> --rpc-url ${networkConfig.rpcUrl} --private-key $PRIVATE_KEY
139434
- cast send ${addresses.billing} "deposit(uint256)" <amount> --rpc-url ${networkConfig.rpcUrl} --private-key $PRIVATE_KEY`
139505
+ ` \u26A0 ${ownerOverride ? "Owner's" : "Your"} Billing balance is 0. Indexing fees are charged from the Billing contract.
139506
+ ${who} must deposit ST tokens into the Billing contract before the processor can run.
139507
+ Example (replace 100 with your desired ST amount):
139508
+ export AMOUNT=$(cast to-wei 100 ether)
139509
+ cast send ${addresses.token} "approve(address,uint256)" ${addresses.billing} $AMOUNT --rpc-url ${networkConfig.rpcUrl} --private-key ${keyHint}
139510
+ cast send ${addresses.billing} "deposit(uint256)" $AMOUNT --rpc-url ${networkConfig.rpcUrl} --private-key ${keyHint}`
139435
139511
  )
139436
139512
  );
139437
139513
  }
@@ -139729,7 +139805,10 @@ function myParseInt(value, dummyPrevious) {
139729
139805
  return parsedValue;
139730
139806
  }
139731
139807
  function createUploadCommand() {
139732
- return new Command("upload").description("Upload processor to Sentio").option("--owner <owner>", "(Optional) Override Project owner").option("--name <name>", "(Optional) Override Project name").option(
139808
+ return new Command("upload").description("Upload processor to Sentio").option(
139809
+ "--owner <owner>",
139810
+ "(Optional) Project owner. Platform mode: project owner username. --no-platform mode: on-chain owner address (0x...) \u2014 $PRIVATE_KEY then signs as an operator on the owner's behalf, requires Permissions.addOperator beforehand."
139811
+ ).option("--name <name>", "(Optional) Override Project name").option(
139733
139812
  "--continue-from <version>",
139734
139813
  "(Optional) Continue processing data from the specific processor version which will keeping the old data from previous version and will STOP that version IMMEDIATELY.",
139735
139814
  myParseInt
@@ -139765,13 +139844,41 @@ async function runNoPlatformUpload(processorConfig, options) {
139765
139844
  const privateKey = requirePrivateKey();
139766
139845
  const wallet = getWalletFromPrivateKey(privateKey);
139767
139846
  const walletAddress = wallet.address;
139847
+ let ownerOverride;
139848
+ if (options.owner) {
139849
+ if (!ethers_exports.isAddress(options.owner)) {
139850
+ console.error(source_default.red(`Invalid --owner: "${options.owner}" is not a valid 0x-prefixed Ethereum address.`));
139851
+ process.exit(1);
139852
+ }
139853
+ ownerOverride = ethers_exports.getAddress(options.owner);
139854
+ }
139855
+ const effectiveOwner = ownerOverride ?? walletAddress;
139768
139856
  console.log(source_default.blue("Resolving contract addresses from AddressBook..."));
139769
139857
  const addresses = await resolveNetworkAddresses(networkConfig);
139858
+ if (ownerOverride) {
139859
+ const isOp = await isOperatorOnChain(networkConfig, addresses, ownerOverride, walletAddress);
139860
+ if (!isOp) {
139861
+ console.error(
139862
+ source_default.red(
139863
+ `Operator ${walletAddress} is not authorized for owner ${ownerOverride}.
139864
+ The owner must call Permissions.addOperator(${walletAddress}) first.`
139865
+ )
139866
+ );
139867
+ process.exit(1);
139868
+ }
139869
+ }
139770
139870
  const [stBalance, billingBalance] = await Promise.all([
139771
- checkSTBalance(networkConfig, addresses, walletAddress),
139772
- checkBillingBalance(networkConfig, addresses, walletAddress)
139871
+ checkSTBalance(networkConfig, addresses, effectiveOwner),
139872
+ checkBillingBalance(networkConfig, addresses, effectiveOwner)
139773
139873
  ]);
139774
- const confirmed = await confirmNoPlatformUpload(walletAddress, stBalance, billingBalance, addresses, networkConfig);
139874
+ const confirmed = await confirmNoPlatformUpload(
139875
+ walletAddress,
139876
+ stBalance,
139877
+ billingBalance,
139878
+ addresses,
139879
+ networkConfig,
139880
+ ownerOverride
139881
+ );
139775
139882
  if (!confirmed) {
139776
139883
  console.log("Upload cancelled.");
139777
139884
  process.exit(0);
@@ -139821,9 +139928,10 @@ async function runNoPlatformUpload(processorConfig, options) {
139821
139928
  const existingProcessor = await getProcessorOnChain(networkConfig, addresses, processorId);
139822
139929
  if (existingProcessor) {
139823
139930
  const existingOwner = existingProcessor.owner.toLowerCase();
139824
- const currentWallet = wallet.address.toLowerCase();
139825
- if (existingOwner === currentWallet) {
139826
- console.log(source_default.yellow(`Processor "${processorId}" already exists (owned by you).`));
139931
+ const expectedOwner = effectiveOwner.toLowerCase();
139932
+ if (existingOwner === expectedOwner) {
139933
+ const ownedBy = ownerOverride ? `owned by ${ownerOverride}` : "owned by you";
139934
+ console.log(source_default.yellow(`Processor "${processorId}" already exists (${ownedBy}).`));
139827
139935
  const shouldReplace = await confirm(`Replace existing processor "${processorId}"?`);
139828
139936
  if (!shouldReplace) {
139829
139937
  console.log("Upload cancelled.");
@@ -139832,9 +139940,10 @@ async function runNoPlatformUpload(processorConfig, options) {
139832
139940
  await stopProcessorOnChain(networkConfig, addresses, wallet, processorId);
139833
139941
  await deleteProcessorOnChain(networkConfig, addresses, wallet, processorId);
139834
139942
  } else {
139943
+ const expectedLabel = ownerOverride ? `expected owner ${ownerOverride}` : `your wallet ${wallet.address}`;
139835
139944
  console.log(
139836
139945
  source_default.yellow(
139837
- `Processor "${processorId}" already exists and is owned by ${existingProcessor.owner} (not your wallet ${wallet.address}).`
139946
+ `Processor "${processorId}" already exists and is owned by ${existingProcessor.owner} (not ${expectedLabel}).`
139838
139947
  )
139839
139948
  );
139840
139949
  const randomSuffix = Math.random().toString(36).substring(2, 8);
@@ -139850,7 +139959,16 @@ async function runNoPlatformUpload(processorConfig, options) {
139850
139959
  console.log(source_default.blue(`Using processor ID: ${processorId}`));
139851
139960
  }
139852
139961
  }
139853
- await createProcessorOnChain(networkConfig, addresses, wallet, processorId, cid, requiredChainIds, sdkVersion);
139962
+ await createProcessorOnChain(
139963
+ networkConfig,
139964
+ addresses,
139965
+ wallet,
139966
+ processorId,
139967
+ cid,
139968
+ requiredChainIds,
139969
+ sdkVersion,
139970
+ ownerOverride
139971
+ );
139854
139972
  await startProcessorOnChain(networkConfig, addresses, wallet, processorId);
139855
139973
  console.log();
139856
139974
  console.log(source_default.green("=== Upload Complete ==="));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sentio/cli",
3
- "version": "3.6.2",
3
+ "version": "3.7.0-rc.1",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "exports": {
@@ -20,6 +20,7 @@ import {
20
20
  requirePrivateKey,
21
21
  checkSTBalance,
22
22
  checkBillingBalance,
23
+ isOperatorOnChain,
23
24
  uploadToIPFS,
24
25
  createProcessorOnChain,
25
26
  startProcessorOnChain,
@@ -28,6 +29,7 @@ import {
28
29
  getProcessorOnChain,
29
30
  deleteProcessorOnChain
30
31
  } from '../network.js'
32
+ import { ethers } from 'ethers'
31
33
  import { Auth, DefaultBatchUploader, FileType, IPFSBatchUploader, WalrusBatchUploader } from '../uploader.js'
32
34
  export { type Auth } from '../uploader.js'
33
35
 
@@ -43,7 +45,10 @@ function myParseInt(value: string, dummyPrevious: number): number {
43
45
  export function createUploadCommand() {
44
46
  return new Command('upload')
45
47
  .description('Upload processor to Sentio')
46
- .option('--owner <owner>', '(Optional) Override Project owner')
48
+ .option(
49
+ '--owner <owner>',
50
+ "(Optional) Project owner. Platform mode: project owner username. --no-platform mode: on-chain owner address (0x...) — $PRIVATE_KEY then signs as an operator on the owner's behalf, requires Permissions.addOperator beforehand."
51
+ )
47
52
  .option('--name <name>', '(Optional) Override Project name')
48
53
  .option(
49
54
  '--continue-from <version>',
@@ -102,21 +107,53 @@ async function runNoPlatformUpload(
102
107
  const network = options.sentioNetwork || 'testnet'
103
108
  const networkConfig = getSentioNetworkConfig(network)
104
109
 
105
- // Step 1: Require PRIVATE_KEY
110
+ // Step 1: Require PRIVATE_KEY (operator key when --owner is set, otherwise the owner's own key)
106
111
  const privateKey = requirePrivateKey()
107
112
  const wallet = getWalletFromPrivateKey(privateKey)
108
113
  const walletAddress = wallet.address
109
114
 
115
+ // Operator mode: --owner is the on-chain owner address; PRIVATE_KEY signs as operator on its behalf.
116
+ let ownerOverride: string | undefined
117
+ if (options.owner) {
118
+ if (!ethers.isAddress(options.owner)) {
119
+ console.error(chalk.red(`Invalid --owner: "${options.owner}" is not a valid 0x-prefixed Ethereum address.`))
120
+ process.exit(1)
121
+ }
122
+ ownerOverride = ethers.getAddress(options.owner)
123
+ }
124
+ const effectiveOwner = ownerOverride ?? walletAddress
125
+
110
126
  // Step 2: Resolve contract addresses via AddressBook
111
127
  console.log(chalk.blue('Resolving contract addresses from AddressBook...'))
112
128
  const addresses = await resolveNetworkAddresses(networkConfig)
113
129
 
114
- // Step 3: Check ST balance and Billing balance, then confirm
130
+ // Step 2.5: Pre-flight check that operator is registered for this owner.
131
+ if (ownerOverride) {
132
+ const isOp = await isOperatorOnChain(networkConfig, addresses, ownerOverride, walletAddress)
133
+ if (!isOp) {
134
+ console.error(
135
+ chalk.red(
136
+ `Operator ${walletAddress} is not authorized for owner ${ownerOverride}.\n` +
137
+ `The owner must call Permissions.addOperator(${walletAddress}) first.`
138
+ )
139
+ )
140
+ process.exit(1)
141
+ }
142
+ }
143
+
144
+ // Step 3: Check ST balance and Billing balance (always against the owner — operator just submits txs)
115
145
  const [stBalance, billingBalance] = await Promise.all([
116
- checkSTBalance(networkConfig, addresses, walletAddress),
117
- checkBillingBalance(networkConfig, addresses, walletAddress)
146
+ checkSTBalance(networkConfig, addresses, effectiveOwner),
147
+ checkBillingBalance(networkConfig, addresses, effectiveOwner)
118
148
  ])
119
- const confirmed = await confirmNoPlatformUpload(walletAddress, stBalance, billingBalance, addresses, networkConfig)
149
+ const confirmed = await confirmNoPlatformUpload(
150
+ walletAddress,
151
+ stBalance,
152
+ billingBalance,
153
+ addresses,
154
+ networkConfig,
155
+ ownerOverride
156
+ )
120
157
  if (!confirmed) {
121
158
  console.log('Upload cancelled.')
122
159
  process.exit(0)
@@ -179,11 +216,13 @@ async function runNoPlatformUpload(
179
216
 
180
217
  if (existingProcessor) {
181
218
  const existingOwner = existingProcessor.owner.toLowerCase()
182
- const currentWallet = wallet.address.toLowerCase()
219
+ const expectedOwner = effectiveOwner.toLowerCase()
183
220
 
184
- if (existingOwner === currentWallet) {
185
- // Same owner — prompt to replace
186
- console.log(chalk.yellow(`Processor "${processorId}" already exists (owned by you).`))
221
+ if (existingOwner === expectedOwner) {
222
+ // Same owner — prompt to replace. In operator mode the operator is allowed to
223
+ // stop+delete via the contract's isProcessorAdmin check.
224
+ const ownedBy = ownerOverride ? `owned by ${ownerOverride}` : 'owned by you'
225
+ console.log(chalk.yellow(`Processor "${processorId}" already exists (${ownedBy}).`))
187
226
  const shouldReplace = await confirm(`Replace existing processor "${processorId}"?`)
188
227
  if (!shouldReplace) {
189
228
  console.log('Upload cancelled.')
@@ -194,9 +233,10 @@ async function runNoPlatformUpload(
194
233
  await deleteProcessorOnChain(networkConfig, addresses, wallet, processorId)
195
234
  } else {
196
235
  // Different owner — prompt to rename
236
+ const expectedLabel = ownerOverride ? `expected owner ${ownerOverride}` : `your wallet ${wallet.address}`
197
237
  console.log(
198
238
  chalk.yellow(
199
- `Processor "${processorId}" already exists and is owned by ${existingProcessor.owner} (not your wallet ${wallet.address}).`
239
+ `Processor "${processorId}" already exists and is owned by ${existingProcessor.owner} (not ${expectedLabel}).`
200
240
  )
201
241
  )
202
242
  const randomSuffix = Math.random().toString(36).substring(2, 8)
@@ -215,7 +255,16 @@ async function runNoPlatformUpload(
215
255
  }
216
256
 
217
257
  // Step 8: Create processor on-chain
218
- await createProcessorOnChain(networkConfig, addresses, wallet, processorId, cid, requiredChainIds, sdkVersion)
258
+ await createProcessorOnChain(
259
+ networkConfig,
260
+ addresses,
261
+ wallet,
262
+ processorId,
263
+ cid,
264
+ requiredChainIds,
265
+ sdkVersion,
266
+ ownerOverride
267
+ )
219
268
 
220
269
  // Step 9: Start processor on-chain
221
270
  await startProcessorOnChain(networkConfig, addresses, wallet, processorId)
package/src/network.ts CHANGED
@@ -40,7 +40,7 @@ const ADDRESS_BOOK_ABI = [
40
40
  'function getAddress(bytes32 id) view returns (address)'
41
41
  ]
42
42
 
43
- // ProcessorRegistry: createProcessor, getProcessor, deleteProcessor
43
+ // ProcessorRegistry: createProcessor (self + operator overloads), getProcessor, deleteProcessor
44
44
  const PROCESSOR_REGISTRY_ABI = [
45
45
  `function createProcessor(
46
46
  string id,
@@ -48,8 +48,14 @@ const PROCESSOR_REGISTRY_ABI = [
48
48
  tuple(string chainId, bool enableRpc, bool enableTrace)[] requireChains,
49
49
  string sdkVersion
50
50
  ) returns (string)`,
51
+ `function createProcessor(
52
+ address owner,
53
+ string id,
54
+ tuple(uint8 sourceType, string ipfsCid) source,
55
+ tuple(string chainId, bool enableRpc, bool enableTrace)[] requireChains,
56
+ string sdkVersion
57
+ ) returns (string)`,
51
58
  'event ProcessorCreated(string indexed processorId)',
52
- 'function getAllocations(string processorId) view returns (tuple(uint256 indexerId, uint256 timestamp, bool indexerReady)[])',
53
59
  `function getProcessor(string processorId) view returns (
54
60
  tuple(
55
61
  string id,
@@ -59,14 +65,24 @@ const PROCESSOR_REGISTRY_ABI = [
59
65
  string sdkVersion,
60
66
  tuple(string chainId, bool enableRpc, bool enableTrace)[] requireChains,
61
67
  tuple(uint8 sourceType, string ipfsCid) source,
62
- tuple(uint256 indexerId, uint256 timestamp, bool indexerReady)[] allocations
68
+ tuple(uint256 indexerId, uint256 allocationTime, bool indexerReady, uint256 replicaIndex)[] allocations
63
69
  )
64
70
  )`,
65
71
  'function deleteProcessor(string processorId)'
66
72
  ]
67
73
 
68
- // Controller: startProcessor / stopProcessor
69
- const CONTROLLER_ABI = ['function startProcessor(string processorId)', 'function stopProcessor(string processorId)']
74
+ // Controller: startProcessor / stopProcessor / getAllocations
75
+ const CONTROLLER_ABI = [
76
+ 'function startProcessor(string processorId)',
77
+ 'function stopProcessor(string processorId)',
78
+ 'function getAllocations(string processorId) view returns (tuple(uint256 indexerId, uint256 allocationTime, bool indexerReady, uint256 replicaIndex)[])'
79
+ ]
80
+
81
+ // Databases: getProcessorDatabases
82
+ const DATABASES_ABI = ['function getProcessorDatabases(string processorId) view returns (string[])']
83
+
84
+ // Permissions: isOperator (used to pre-check operator delegation before submitting tx)
85
+ const PERMISSIONS_ABI = ['function isOperator(address account, address operator) view returns (bool)']
70
86
 
71
87
  // ERC20: balanceOf + approve
72
88
  const ERC20_ABI = [
@@ -84,7 +100,9 @@ const ADDRESS_BOOK_KEYS = {
84
100
  processorRegistry: 'processor_registry',
85
101
  controller: 'controller',
86
102
  token: 'sentio_token',
87
- billing: 'billing'
103
+ billing: 'billing',
104
+ databases: 'databases',
105
+ permissions: 'permissions'
88
106
  } as const
89
107
 
90
108
  interface ResolvedAddresses {
@@ -93,6 +111,8 @@ interface ResolvedAddresses {
93
111
  controller: string
94
112
  token: string
95
113
  billing: string
114
+ databases: string
115
+ permissions: string
96
116
  }
97
117
 
98
118
  let cachedAddresses: ResolvedAddresses | undefined
@@ -122,19 +142,31 @@ export async function resolveNetworkAddresses(config: SentioNetworkConfig): Prom
122
142
  }
123
143
  }
124
144
 
125
- const [processorRegistry, controller, token, billing] = await Promise.all([
145
+ const [processorRegistry, controller, token, billing, databases, permissions] = await Promise.all([
126
146
  resolveAddress(ADDRESS_BOOK_KEYS.processorRegistry),
127
147
  resolveAddress(ADDRESS_BOOK_KEYS.controller),
128
148
  resolveAddress(ADDRESS_BOOK_KEYS.token),
129
- resolveAddress(ADDRESS_BOOK_KEYS.billing)
149
+ resolveAddress(ADDRESS_BOOK_KEYS.billing),
150
+ resolveAddress(ADDRESS_BOOK_KEYS.databases),
151
+ resolveAddress(ADDRESS_BOOK_KEYS.permissions)
130
152
  ])
131
153
 
132
154
  console.log(chalk.gray(`ProcessorRegistry: ${processorRegistry}`))
133
155
  console.log(chalk.gray(`Controller: ${controller}`))
134
156
  console.log(chalk.gray(`ST Token: ${token}`))
135
157
  console.log(chalk.gray(`Billing: ${billing}`))
136
-
137
- cachedAddresses = { addressBook: addressBookAddr, processorRegistry, controller, token, billing }
158
+ console.log(chalk.gray(`Databases: ${databases}`))
159
+ console.log(chalk.gray(`Permissions: ${permissions}`))
160
+
161
+ cachedAddresses = {
162
+ addressBook: addressBookAddr,
163
+ processorRegistry,
164
+ controller,
165
+ token,
166
+ billing,
167
+ databases,
168
+ permissions
169
+ }
138
170
  return cachedAddresses
139
171
  }
140
172
 
@@ -186,6 +218,17 @@ export async function checkBillingBalance(
186
218
  }
187
219
  }
188
220
 
221
+ export async function isOperatorOnChain(
222
+ config: SentioNetworkConfig,
223
+ addresses: ResolvedAddresses,
224
+ owner: string,
225
+ operator: string
226
+ ): Promise<boolean> {
227
+ const provider = new ethers.JsonRpcProvider(config.rpcUrl)
228
+ const permissions = new ethers.Contract(addresses.permissions, PERMISSIONS_ABI, provider)
229
+ return await permissions.isOperator(owner, operator)
230
+ }
231
+
189
232
  // --- IPFS Upload ---
190
233
 
191
234
  export async function uploadToIPFS(fileBuffer: Buffer, ipfsUrl: string): Promise<string> {
@@ -207,6 +250,37 @@ export async function uploadToIPFS(fileBuffer: Buffer, ipfsUrl: string): Promise
207
250
  return result.Hash
208
251
  }
209
252
 
253
+ // --- Tx Helpers ---
254
+
255
+ async function submitAndWait(
256
+ explorerUrl: string,
257
+ label: string,
258
+ txPromise: Promise<ethers.TransactionResponse>
259
+ ): Promise<ethers.TransactionResponse> {
260
+ let tx: ethers.TransactionResponse
261
+ try {
262
+ tx = await txPromise
263
+ } catch (err: any) {
264
+ throw new Error(`${label} failed to submit: ${err.shortMessage ?? err.message}`)
265
+ }
266
+
267
+ console.log(chalk.gray(` Tx hash: ${explorerUrl}/tx/${tx.hash}`))
268
+ console.log(chalk.blue('Waiting for confirmation...'))
269
+
270
+ let receipt: ethers.TransactionReceipt | null
271
+ try {
272
+ receipt = await tx.wait()
273
+ } catch (err: any) {
274
+ throw new Error(`${label} failed: ${err.shortMessage ?? err.message}. Tx: ${explorerUrl}/tx/${tx.hash}`)
275
+ }
276
+
277
+ if (!receipt || receipt.status === 0) {
278
+ throw new Error(`${label} transaction reverted. Tx: ${explorerUrl}/tx/${tx.hash}`)
279
+ }
280
+
281
+ return tx
282
+ }
283
+
210
284
  // --- Contract Interactions ---
211
285
 
212
286
  export interface OnChainProcessor {
@@ -260,17 +334,45 @@ export async function deleteProcessorOnChain(
260
334
  const registry = new ethers.Contract(addresses.processorRegistry, PROCESSOR_REGISTRY_ABI, signer)
261
335
 
262
336
  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...'))
337
+ const tx = await submitAndWait(config.explorerUrl, 'deleteProcessor', registry.deleteProcessor(processorId))
338
+ console.log(chalk.green(`Processor deleted. Tx: ${config.explorerUrl}/tx/${tx.hash}`))
339
+ await waitForProcessorDatabaseCleanup(addresses, processorId, provider)
340
+
341
+ return tx.hash
342
+ }
266
343
 
267
- const receipt = await tx.wait()
268
- if (receipt.status === 0) {
269
- throw new Error(`deleteProcessor transaction failed. Tx: ${config.explorerUrl}/tx/${tx.hash}`)
344
+ async function waitForProcessorDatabaseCleanup(
345
+ addresses: ResolvedAddresses,
346
+ processorId: string,
347
+ provider: ethers.JsonRpcProvider,
348
+ timeoutMs = 120_000,
349
+ pollIntervalMs = 5_000
350
+ ): Promise<void> {
351
+ const databases = new ethers.Contract(addresses.databases, DATABASES_ABI, provider)
352
+
353
+ // getProcessorDatabases has no active check — returns all db IDs still in storage.
354
+ // Only when this returns empty is it safe to recreate databases with the same IDs.
355
+ const dbIds: string[] = await databases.getProcessorDatabases(processorId)
356
+ if (dbIds.length === 0) {
357
+ return
270
358
  }
271
359
 
272
- console.log(chalk.green(`Processor deleted. Tx: ${config.explorerUrl}/tx/${tx.hash}`))
273
- return tx.hash
360
+ console.log(chalk.blue(`Waiting for ${dbIds.length} replica database(s) to be cleaned up...`))
361
+
362
+ const deadline = Date.now() + timeoutMs
363
+ while (Date.now() < deadline) {
364
+ await new Promise((resolve) => setTimeout(resolve, pollIntervalMs))
365
+
366
+ const remaining: string[] = await databases.getProcessorDatabases(processorId)
367
+ if (remaining.length === 0) {
368
+ console.log(chalk.green('All replica databases cleaned up.'))
369
+ return
370
+ }
371
+
372
+ console.log(chalk.gray(` ${remaining.length} replica database(s) still pending cleanup...`))
373
+ }
374
+
375
+ throw new Error(`Timeout: processor databases not cleaned up within ${timeoutMs / 1000}s`)
274
376
  }
275
377
 
276
378
  export async function createProcessorOnChain(
@@ -280,7 +382,8 @@ export async function createProcessorOnChain(
280
382
  processorId: string,
281
383
  ipfsCid: string,
282
384
  requiredChainIds: string[],
283
- sdkVersion: string
385
+ sdkVersion: string,
386
+ ownerOverride?: string
284
387
  ): Promise<string> {
285
388
  const provider = new ethers.JsonRpcProvider(config.rpcUrl)
286
389
  const signer = wallet.connect(provider)
@@ -303,16 +406,26 @@ export async function createProcessorOnChain(
303
406
  console.log(chalk.gray(` IPFS CID: ${ipfsCid}`))
304
407
  console.log(chalk.gray(` Chains: ${requiredChainIds.join(', ')}`))
305
408
  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}`)
409
+ if (ownerOverride) {
410
+ console.log(chalk.gray(` Owner: ${ownerOverride} (operator: ${wallet.address})`))
314
411
  }
315
412
 
413
+ // ethers v6 disambiguates overloads by full signature when both are present.
414
+ const txPromise: Promise<ethers.TransactionResponse> = ownerOverride
415
+ ? registry['createProcessor(address,string,(uint8,string),(string,bool,bool)[],string)'](
416
+ ownerOverride,
417
+ processorId,
418
+ source,
419
+ requireChains,
420
+ sdkVersion
421
+ )
422
+ : registry['createProcessor(string,(uint8,string),(string,bool,bool)[],string)'](
423
+ processorId,
424
+ source,
425
+ requireChains,
426
+ sdkVersion
427
+ )
428
+ const tx = await submitAndWait(config.explorerUrl, 'createProcessor', txPromise)
316
429
  console.log(chalk.green(`Processor created. Tx: ${config.explorerUrl}/tx/${tx.hash}`))
317
430
  return tx.hash
318
431
  }
@@ -329,15 +442,7 @@ export async function startProcessorOnChain(
329
442
  const controller = new ethers.Contract(addresses.controller, CONTROLLER_ABI, signer)
330
443
 
331
444
  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
-
445
+ const tx = await submitAndWait(config.explorerUrl, 'startProcessor', controller.startProcessor(processorId))
341
446
  console.log(chalk.green(`Processor started. Tx: ${config.explorerUrl}/tx/${tx.hash}`))
342
447
  return tx.hash
343
448
  }
@@ -353,16 +458,15 @@ export async function stopProcessorOnChain(
353
458
 
354
459
  const controller = new ethers.Contract(addresses.controller, CONTROLLER_ABI, signer)
355
460
 
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}`)
461
+ // stopProcessor reverts with ProcessorNotAllocated if the processor has no active allocations
462
+ const allocations: unknown[] = await controller.getAllocations(processorId)
463
+ if (allocations.length === 0) {
464
+ console.log(chalk.gray('Processor has no allocations, skipping stopProcessor.'))
465
+ return ''
364
466
  }
365
467
 
468
+ console.log(chalk.blue('Stopping processor on-chain...'))
469
+ const tx = await submitAndWait(config.explorerUrl, 'stopProcessor', controller.stopProcessor(processorId))
366
470
  console.log(chalk.green(`Processor stopped. Tx: ${config.explorerUrl}/tx/${tx.hash}`))
367
471
  return tx.hash
368
472
  }
@@ -374,24 +478,34 @@ export async function confirmNoPlatformUpload(
374
478
  stBalance: bigint,
375
479
  billingBalance: bigint,
376
480
  addresses: ResolvedAddresses,
377
- networkConfig: SentioNetworkConfig
481
+ networkConfig: SentioNetworkConfig,
482
+ ownerOverride?: string
378
483
  ): Promise<boolean> {
379
484
  const formattedST = ethers.formatEther(stBalance)
380
485
  const formattedBilling = ethers.formatEther(billingBalance)
381
486
  console.log()
382
487
  console.log(chalk.blue('=== Sentio Network Direct Upload (No Platform) ==='))
383
- console.log(chalk.white(` Address: ${walletAddress}`))
488
+ if (ownerOverride) {
489
+ console.log(chalk.white(` Owner: ${ownerOverride}`))
490
+ console.log(chalk.white(` Operator: ${walletAddress}`))
491
+ } else {
492
+ console.log(chalk.white(` Address: ${walletAddress}`))
493
+ }
384
494
  console.log(chalk.white(` ST Balance: ${formattedST} ST`))
385
495
  console.log(chalk.white(` Billing Balance: ${formattedBilling} ST`))
386
496
 
387
497
  if (billingBalance === 0n) {
498
+ const who = ownerOverride ? `the owner (${ownerOverride})` : 'you'
499
+ const keyHint = ownerOverride ? "<owner's private key>" : '$PRIVATE_KEY'
388
500
  console.log()
389
501
  console.log(
390
502
  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`
503
+ `${ownerOverride ? "Owner's" : 'Your'} Billing balance is 0. Indexing fees are charged from the Billing contract.\n` +
504
+ ` ${who} must deposit ST tokens into the Billing contract before the processor can run.\n` +
505
+ ` Example (replace 100 with your desired ST amount):\n` +
506
+ ` export AMOUNT=$(cast to-wei 100 ether)\n` +
507
+ ` cast send ${addresses.token} "approve(address,uint256)" ${addresses.billing} $AMOUNT --rpc-url ${networkConfig.rpcUrl} --private-key ${keyHint}\n` +
508
+ ` cast send ${addresses.billing} "deposit(uint256)" $AMOUNT --rpc-url ${networkConfig.rpcUrl} --private-key ${keyHint}`
395
509
  )
396
510
  )
397
511
  }