@jinn-network/client 0.1.0-canary.adfd078d

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.
Files changed (278) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +162 -0
  3. package/deployments/deployment-phase1a-l2-baseSepolia-fast.json +32 -0
  4. package/deployments/deployment-phase1a-token-baseSepolia-fast.json +27 -0
  5. package/deployments/deployment-phase1b-mech-baseSepolia-fast.json +26 -0
  6. package/deployments/deployment-stolas-l2-baseSepolia-fast.json +35 -0
  7. package/dist/adapters/adapter.d.ts +11 -0
  8. package/dist/adapters/adapter.js +2 -0
  9. package/dist/adapters/adapter.js.map +1 -0
  10. package/dist/adapters/local/adapter.d.ts +20 -0
  11. package/dist/adapters/local/adapter.js +146 -0
  12. package/dist/adapters/local/adapter.js.map +1 -0
  13. package/dist/adapters/mech/adapter.d.ts +29 -0
  14. package/dist/adapters/mech/adapter.js +332 -0
  15. package/dist/adapters/mech/adapter.js.map +1 -0
  16. package/dist/adapters/mech/claim-policy.d.ts +40 -0
  17. package/dist/adapters/mech/claim-policy.js +104 -0
  18. package/dist/adapters/mech/claim-policy.js.map +1 -0
  19. package/dist/adapters/mech/contracts.d.ts +44 -0
  20. package/dist/adapters/mech/contracts.js +323 -0
  21. package/dist/adapters/mech/contracts.js.map +1 -0
  22. package/dist/adapters/mech/ipfs.d.ts +43 -0
  23. package/dist/adapters/mech/ipfs.js +142 -0
  24. package/dist/adapters/mech/ipfs.js.map +1 -0
  25. package/dist/adapters/mech/safe.d.ts +15 -0
  26. package/dist/adapters/mech/safe.js +113 -0
  27. package/dist/adapters/mech/safe.js.map +1 -0
  28. package/dist/adapters/mech/types.d.ts +561 -0
  29. package/dist/adapters/mech/types.js +340 -0
  30. package/dist/adapters/mech/types.js.map +1 -0
  31. package/dist/api/balance-build.d.ts +22 -0
  32. package/dist/api/balance-build.js +37 -0
  33. package/dist/api/balance-build.js.map +1 -0
  34. package/dist/api/fleet-build.d.ts +62 -0
  35. package/dist/api/fleet-build.js +91 -0
  36. package/dist/api/fleet-build.js.map +1 -0
  37. package/dist/api/gather-status.d.ts +20 -0
  38. package/dist/api/gather-status.js +137 -0
  39. package/dist/api/gather-status.js.map +1 -0
  40. package/dist/api/history-build.d.ts +32 -0
  41. package/dist/api/history-build.js +48 -0
  42. package/dist/api/history-build.js.map +1 -0
  43. package/dist/api/peers.d.ts +27 -0
  44. package/dist/api/peers.js +94 -0
  45. package/dist/api/peers.js.map +1 -0
  46. package/dist/api/rewards-build.d.ts +20 -0
  47. package/dist/api/rewards-build.js +42 -0
  48. package/dist/api/rewards-build.js.map +1 -0
  49. package/dist/api/server.d.ts +34 -0
  50. package/dist/api/server.js +130 -0
  51. package/dist/api/server.js.map +1 -0
  52. package/dist/api/status-build.d.ts +92 -0
  53. package/dist/api/status-build.js +175 -0
  54. package/dist/api/status-build.js.map +1 -0
  55. package/dist/api/status-rollup-build.d.ts +36 -0
  56. package/dist/api/status-rollup-build.js +69 -0
  57. package/dist/api/status-rollup-build.js.map +1 -0
  58. package/dist/auth/erc8128.d.ts +43 -0
  59. package/dist/auth/erc8128.js +88 -0
  60. package/dist/auth/erc8128.js.map +1 -0
  61. package/dist/bin/jinn.d.ts +11 -0
  62. package/dist/bin/jinn.js +20 -0
  63. package/dist/bin/jinn.js.map +1 -0
  64. package/dist/build-meta.json +3 -0
  65. package/dist/chain-read-errors.d.ts +9 -0
  66. package/dist/chain-read-errors.js +43 -0
  67. package/dist/chain-read-errors.js.map +1 -0
  68. package/dist/cli/action.d.ts +26 -0
  69. package/dist/cli/action.js +56 -0
  70. package/dist/cli/action.js.map +1 -0
  71. package/dist/cli/command.d.ts +62 -0
  72. package/dist/cli/command.js +29 -0
  73. package/dist/cli/command.js.map +1 -0
  74. package/dist/cli/commands/balance.d.ts +3 -0
  75. package/dist/cli/commands/balance.js +46 -0
  76. package/dist/cli/commands/balance.js.map +1 -0
  77. package/dist/cli/commands/bootstrap.d.ts +3 -0
  78. package/dist/cli/commands/bootstrap.js +165 -0
  79. package/dist/cli/commands/bootstrap.js.map +1 -0
  80. package/dist/cli/commands/claim-rewards.d.ts +3 -0
  81. package/dist/cli/commands/claim-rewards.js +121 -0
  82. package/dist/cli/commands/claim-rewards.js.map +1 -0
  83. package/dist/cli/commands/doctor.d.ts +3 -0
  84. package/dist/cli/commands/doctor.js +151 -0
  85. package/dist/cli/commands/doctor.js.map +1 -0
  86. package/dist/cli/commands/fleet-scale.d.ts +3 -0
  87. package/dist/cli/commands/fleet-scale.js +449 -0
  88. package/dist/cli/commands/fleet-scale.js.map +1 -0
  89. package/dist/cli/commands/fleet.d.ts +3 -0
  90. package/dist/cli/commands/fleet.js +45 -0
  91. package/dist/cli/commands/fleet.js.map +1 -0
  92. package/dist/cli/commands/fund-requirements.d.ts +3 -0
  93. package/dist/cli/commands/fund-requirements.js +139 -0
  94. package/dist/cli/commands/fund-requirements.js.map +1 -0
  95. package/dist/cli/commands/history.d.ts +3 -0
  96. package/dist/cli/commands/history.js +61 -0
  97. package/dist/cli/commands/history.js.map +1 -0
  98. package/dist/cli/commands/init.d.ts +3 -0
  99. package/dist/cli/commands/init.js +91 -0
  100. package/dist/cli/commands/init.js.map +1 -0
  101. package/dist/cli/commands/keys-backup.d.ts +3 -0
  102. package/dist/cli/commands/keys-backup.js +107 -0
  103. package/dist/cli/commands/keys-backup.js.map +1 -0
  104. package/dist/cli/commands/logs.d.ts +3 -0
  105. package/dist/cli/commands/logs.js +69 -0
  106. package/dist/cli/commands/logs.js.map +1 -0
  107. package/dist/cli/commands/rewards.d.ts +3 -0
  108. package/dist/cli/commands/rewards.js +54 -0
  109. package/dist/cli/commands/rewards.js.map +1 -0
  110. package/dist/cli/commands/run.d.ts +3 -0
  111. package/dist/cli/commands/run.js +96 -0
  112. package/dist/cli/commands/run.js.map +1 -0
  113. package/dist/cli/commands/status.d.ts +3 -0
  114. package/dist/cli/commands/status.js +54 -0
  115. package/dist/cli/commands/status.js.map +1 -0
  116. package/dist/cli/commands/stop.d.ts +3 -0
  117. package/dist/cli/commands/stop.js +82 -0
  118. package/dist/cli/commands/stop.js.map +1 -0
  119. package/dist/cli/commands/submit-intent.d.ts +3 -0
  120. package/dist/cli/commands/submit-intent.js +169 -0
  121. package/dist/cli/commands/submit-intent.js.map +1 -0
  122. package/dist/cli/commands/version.d.ts +3 -0
  123. package/dist/cli/commands/version.js +114 -0
  124. package/dist/cli/commands/version.js.map +1 -0
  125. package/dist/cli/commands/withdraw.d.ts +3 -0
  126. package/dist/cli/commands/withdraw.js +181 -0
  127. package/dist/cli/commands/withdraw.js.map +1 -0
  128. package/dist/cli/deployment-digest.d.ts +10 -0
  129. package/dist/cli/deployment-digest.js +25 -0
  130. package/dist/cli/deployment-digest.js.map +1 -0
  131. package/dist/cli/execution-context.d.ts +50 -0
  132. package/dist/cli/execution-context.js +154 -0
  133. package/dist/cli/execution-context.js.map +1 -0
  134. package/dist/cli/help.d.ts +3 -0
  135. package/dist/cli/help.js +37 -0
  136. package/dist/cli/help.js.map +1 -0
  137. package/dist/cli/index.d.ts +17 -0
  138. package/dist/cli/index.js +132 -0
  139. package/dist/cli/index.js.map +1 -0
  140. package/dist/cli/introspection-context.d.ts +10 -0
  141. package/dist/cli/introspection-context.js +60 -0
  142. package/dist/cli/introspection-context.js.map +1 -0
  143. package/dist/cli/output.d.ts +36 -0
  144. package/dist/cli/output.js +35 -0
  145. package/dist/cli/output.js.map +1 -0
  146. package/dist/cli/password.d.ts +12 -0
  147. package/dist/cli/password.js +51 -0
  148. package/dist/cli/password.js.map +1 -0
  149. package/dist/config.d.ts +174 -0
  150. package/dist/config.js +252 -0
  151. package/dist/config.js.map +1 -0
  152. package/dist/daemon/creator.d.ts +24 -0
  153. package/dist/daemon/creator.js +80 -0
  154. package/dist/daemon/creator.js.map +1 -0
  155. package/dist/daemon/daemon.d.ts +60 -0
  156. package/dist/daemon/daemon.js +158 -0
  157. package/dist/daemon/daemon.js.map +1 -0
  158. package/dist/daemon/delivery-watcher.d.ts +10 -0
  159. package/dist/daemon/delivery-watcher.js +37 -0
  160. package/dist/daemon/delivery-watcher.js.map +1 -0
  161. package/dist/daemon/restorer.d.ts +19 -0
  162. package/dist/daemon/restorer.js +82 -0
  163. package/dist/daemon/restorer.js.map +1 -0
  164. package/dist/daemon/reward-claim-loop.d.ts +38 -0
  165. package/dist/daemon/reward-claim-loop.js +48 -0
  166. package/dist/daemon/reward-claim-loop.js.map +1 -0
  167. package/dist/discovery/registry.d.ts +43 -0
  168. package/dist/discovery/registry.js +104 -0
  169. package/dist/discovery/registry.js.map +1 -0
  170. package/dist/discovery/subgraph.d.ts +37 -0
  171. package/dist/discovery/subgraph.js +87 -0
  172. package/dist/discovery/subgraph.js.map +1 -0
  173. package/dist/earning/bootstrap.d.ts +79 -0
  174. package/dist/earning/bootstrap.js +989 -0
  175. package/dist/earning/bootstrap.js.map +1 -0
  176. package/dist/earning/contracts.d.ts +431 -0
  177. package/dist/earning/contracts.js +518 -0
  178. package/dist/earning/contracts.js.map +1 -0
  179. package/dist/earning/evidence-simhash.d.ts +59 -0
  180. package/dist/earning/evidence-simhash.js +87 -0
  181. package/dist/earning/evidence-simhash.js.map +1 -0
  182. package/dist/earning/fleet-display-index.d.ts +8 -0
  183. package/dist/earning/fleet-display-index.js +12 -0
  184. package/dist/earning/fleet-display-index.js.map +1 -0
  185. package/dist/earning/fleet-retire.d.ts +28 -0
  186. package/dist/earning/fleet-retire.js +75 -0
  187. package/dist/earning/fleet-retire.js.map +1 -0
  188. package/dist/earning/jinn-rewards.d.ts +62 -0
  189. package/dist/earning/jinn-rewards.js +81 -0
  190. package/dist/earning/jinn-rewards.js.map +1 -0
  191. package/dist/earning/next-service-index.d.ts +4 -0
  192. package/dist/earning/next-service-index.js +7 -0
  193. package/dist/earning/next-service-index.js.map +1 -0
  194. package/dist/earning/orphan-sweep.d.ts +33 -0
  195. package/dist/earning/orphan-sweep.js +157 -0
  196. package/dist/earning/orphan-sweep.js.map +1 -0
  197. package/dist/earning/reconcile.d.ts +37 -0
  198. package/dist/earning/reconcile.js +216 -0
  199. package/dist/earning/reconcile.js.map +1 -0
  200. package/dist/earning/safe-adapter.d.ts +70 -0
  201. package/dist/earning/safe-adapter.js +228 -0
  202. package/dist/earning/safe-adapter.js.map +1 -0
  203. package/dist/earning/stolas-claim.d.ts +47 -0
  204. package/dist/earning/stolas-claim.js +115 -0
  205. package/dist/earning/stolas-claim.js.map +1 -0
  206. package/dist/earning/store.d.ts +36 -0
  207. package/dist/earning/store.js +156 -0
  208. package/dist/earning/store.js.map +1 -0
  209. package/dist/earning/types.d.ts +123 -0
  210. package/dist/earning/types.js +64 -0
  211. package/dist/earning/types.js.map +1 -0
  212. package/dist/earning/viem-clients.d.ts +9 -0
  213. package/dist/earning/viem-clients.js +22 -0
  214. package/dist/earning/viem-clients.js.map +1 -0
  215. package/dist/earning/wallet.d.ts +20 -0
  216. package/dist/earning/wallet.js +103 -0
  217. package/dist/earning/wallet.js.map +1 -0
  218. package/dist/errors/envelope.d.ts +41 -0
  219. package/dist/errors/envelope.js +48 -0
  220. package/dist/errors/envelope.js.map +1 -0
  221. package/dist/index.d.ts +14 -0
  222. package/dist/index.js +17 -0
  223. package/dist/index.js.map +1 -0
  224. package/dist/main.d.ts +32 -0
  225. package/dist/main.js +281 -0
  226. package/dist/main.js.map +1 -0
  227. package/dist/mcp/server.d.ts +14 -0
  228. package/dist/mcp/server.js +205 -0
  229. package/dist/mcp/server.js.map +1 -0
  230. package/dist/operator-errors.d.ts +16 -0
  231. package/dist/operator-errors.js +88 -0
  232. package/dist/operator-errors.js.map +1 -0
  233. package/dist/preflight/claude-binary.d.ts +19 -0
  234. package/dist/preflight/claude-binary.js +44 -0
  235. package/dist/preflight/claude-binary.js.map +1 -0
  236. package/dist/preflight/claude-invocation-envelope.d.ts +11 -0
  237. package/dist/preflight/claude-invocation-envelope.js +67 -0
  238. package/dist/preflight/claude-invocation-envelope.js.map +1 -0
  239. package/dist/runner/claude.d.ts +15 -0
  240. package/dist/runner/claude.js +193 -0
  241. package/dist/runner/claude.js.map +1 -0
  242. package/dist/runner/runner.d.ts +11 -0
  243. package/dist/runner/runner.js +2 -0
  244. package/dist/runner/runner.js.map +1 -0
  245. package/dist/runner/simple.d.ts +8 -0
  246. package/dist/runner/simple.js +11 -0
  247. package/dist/runner/simple.js.map +1 -0
  248. package/dist/store/store.d.ts +74 -0
  249. package/dist/store/store.js +173 -0
  250. package/dist/store/store.js.map +1 -0
  251. package/dist/tx-retry.d.ts +55 -0
  252. package/dist/tx-retry.js +214 -0
  253. package/dist/tx-retry.js.map +1 -0
  254. package/dist/types/desired-state.d.ts +41 -0
  255. package/dist/types/desired-state.js +16 -0
  256. package/dist/types/desired-state.js.map +1 -0
  257. package/dist/types/errors.d.ts +8 -0
  258. package/dist/types/errors.js +17 -0
  259. package/dist/types/errors.js.map +1 -0
  260. package/dist/types/index.d.ts +2 -0
  261. package/dist/types/index.js +3 -0
  262. package/dist/types/index.js.map +1 -0
  263. package/dist/withdraw/args.d.ts +29 -0
  264. package/dist/withdraw/args.js +198 -0
  265. package/dist/withdraw/args.js.map +1 -0
  266. package/dist/withdraw/run-withdraw-plan.d.ts +21 -0
  267. package/dist/withdraw/run-withdraw-plan.js +257 -0
  268. package/dist/withdraw/run-withdraw-plan.js.map +1 -0
  269. package/dist/x402/acquire.d.ts +6 -0
  270. package/dist/x402/acquire.js +32 -0
  271. package/dist/x402/acquire.js.map +1 -0
  272. package/dist/x402/facilitator.d.ts +11 -0
  273. package/dist/x402/facilitator.js +52 -0
  274. package/dist/x402/facilitator.js.map +1 -0
  275. package/dist/x402/handler.d.ts +15 -0
  276. package/dist/x402/handler.js +40 -0
  277. package/dist/x402/handler.js.map +1 -0
  278. package/package.json +72 -0
@@ -0,0 +1,989 @@
1
+ /**
2
+ * Fleet bootstrap state machine.
3
+ *
4
+ * Phase 1 (master): generate mnemonic → fund master EOA
5
+ * Phase 2 (per-service): derive agent → stake → deploy mech
6
+ */
7
+ import { decodeEventLog, encodeAbiParameters, encodeFunctionData, formatEther, getAddress, zeroAddress, } from 'viem';
8
+ import { ERC20_ABI, EVENT_TOPICS, SERVICE_MANAGER_ABI, SERVICE_REGISTRY_APPROVE_ABI, SERVICE_REGISTRY_L2_ABI, STAKING_ABI, MECH_MARKETPLACE_CREATE_ABI, STOLAS_DISTRIBUTOR_ABI, STOLAS_STAKING_SLOTS_ABI, cidToBytes32, getChainConfig, } from './contracts.js';
9
+ import { executeSafeTxBatch, executeSafeTxDirect, initDeployedSafe, initPredictedSafe, } from './safe-adapter.js';
10
+ import { FleetStateStore } from './store.js';
11
+ import { generateMnemonic, encryptMnemonic, decryptMnemonic, deriveMasterAddress, deriveMasterSigner, deriveAgentAddress, deriveAgentSigner, walletPrivateKeyAtIndex, } from './wallet.js';
12
+ import { createDefaultServiceState } from './types.js';
13
+ import { formatBootstrapOperatorMessage, isJinnDebug, } from '../operator-errors.js';
14
+ import { reconcileServiceAgainstChain, } from './reconcile.js';
15
+ import { previousSafeBeingAbandoned, sweepOrphanedServiceFunds, } from './orphan-sweep.js';
16
+ import { viemSendTransactionWithRetry, waitForTransactionReceiptWithRetry, } from '../tx-retry.js';
17
+ import { createJinnPublicClient, createJinnWalletClient } from './viem-clients.js';
18
+ import { isTransientEthReadError } from '../chain-read-errors.js';
19
+ import { nextFleetServiceIndex } from './next-service-index.js';
20
+ const addr = (value) => getAddress(value);
21
+ const SAFE_TOKEN_BOOTSTRAP_MULTIPLIER = 2n;
22
+ /** Conservative default: ~0.001 ETH/day master gas if not configured. */
23
+ const DEFAULT_MASTER_ETH_DAILY_WEI = 1000000000000000n;
24
+ /** Warn when ETH above the minimum would last fewer than this many days at the daily estimate. */
25
+ const MASTER_ETH_RUNWAY_WARN_DAYS = 7n;
26
+ export class FleetBootstrapper {
27
+ store;
28
+ config;
29
+ publicClient;
30
+ chain;
31
+ stakingMode;
32
+ targetServices;
33
+ debug;
34
+ masterEthDailyEstimateWei;
35
+ constructor(options = {}) {
36
+ this.store = new FleetStateStore(options.earningDir);
37
+ this.chain = options.chain ?? 'base';
38
+ this.stakingMode = options.stakingMode ?? 'standard';
39
+ this.targetServices = options.targetServices ?? 1;
40
+ this.debug = options.debug ?? isJinnDebug();
41
+ const dailyOpt = options.masterEthDailyEstimateWei;
42
+ this.masterEthDailyEstimateWei =
43
+ dailyOpt !== undefined
44
+ ? BigInt(dailyOpt)
45
+ : this.estimateMasterDailyGasWei(options.pollIntervalMs);
46
+ this.config = getChainConfig(this.chain, {
47
+ testnetL2DeploymentPath: options.testnetL2DeploymentPath,
48
+ testnetL2TokenDeploymentPath: options.testnetL2TokenDeploymentPath,
49
+ testnetMechDeploymentPath: options.testnetMechDeploymentPath,
50
+ testnetStolasDeploymentPath: options.testnetStolasDeploymentPath,
51
+ });
52
+ if (options.rpcUrl) {
53
+ this.config.rpcUrl = options.rpcUrl;
54
+ }
55
+ this.publicClient = createJinnPublicClient(this.config.rpcUrl, this.chain);
56
+ }
57
+ async getStatus() {
58
+ return this.store.load(this.chain);
59
+ }
60
+ /**
61
+ * Conservative daily master gas (wei): max(DEFAULT, rough tx count from poll interval × cost).
62
+ */
63
+ estimateMasterDailyGasWei(pollIntervalMs) {
64
+ const interval = Math.max(pollIntervalMs ?? 5000, 1000);
65
+ const pollsPerDay = 86400000 / interval;
66
+ // Assume at most one funding-sized tx per ~600 polls (~50 min at 5s), capped at 12/day
67
+ const txsPerDay = Math.min(Math.ceil(pollsPerDay / 600), 12);
68
+ const txCostWei = 150000n * 2000000000n; // ~150k gas @ 2 gwei
69
+ const fromPoll = BigInt(txsPerDay) * txCostWei;
70
+ return fromPoll > DEFAULT_MASTER_ETH_DAILY_WEI ? fromPoll : DEFAULT_MASTER_ETH_DAILY_WEI;
71
+ }
72
+ async bootstrap(password) {
73
+ // Handle legacy keystore migration
74
+ if (!this.store.hasMnemonicKeystore() && this.store.hasLegacyKeystore()) {
75
+ await this.store.migrateLegacyFiles();
76
+ }
77
+ let state = await this.store.load(this.chain);
78
+ try {
79
+ // Phase 1: Master wallet setup
80
+ state = await this.ensureMasterWallet(state, password);
81
+ // Phase 1b: Check master funding
82
+ const masterAddress = state.master_address;
83
+ const masterBalance = await this.publicClient.getBalance({ address: masterAddress });
84
+ // Self-bond mode needs much more ETH than standard mode because the master
85
+ // funds the agent which then pays for: Safe deploy, 5 service registry txs
86
+ // (create, activate, register, deploy, stake), and mech deploy. Roughly
87
+ // 15 txs at varying gas costs. 0.03 ETH per service is a safe estimate.
88
+ // On re-runs, include ETH already held by funded agents/safes in the total.
89
+ const SELF_BOND_ETH_PER_SERVICE = 30000000000000000n; // 0.03 ETH
90
+ let systemEth = masterBalance;
91
+ if (this.stakingMode === 'self-bond') {
92
+ for (const svc of state.services) {
93
+ if (svc.agent_address) {
94
+ systemEth += await this.publicClient.getBalance({
95
+ address: getAddress(svc.agent_address),
96
+ });
97
+ }
98
+ if (svc.safe_address) {
99
+ systemEth += await this.publicClient.getBalance({
100
+ address: getAddress(svc.safe_address),
101
+ });
102
+ }
103
+ }
104
+ }
105
+ const requiredMasterEth = this.stakingMode === 'standard'
106
+ ? this.config.minEoaGasEth
107
+ : SELF_BOND_ETH_PER_SERVICE * BigInt(this.targetServices);
108
+ if (systemEth < requiredMasterEth) {
109
+ const shortfall = requiredMasterEth - systemEth;
110
+ const friendly = `Your master wallet needs more ETH (currently ${formatEther(masterBalance)} ETH, need ${formatEther(shortfall)} ETH more). Please send ETH to: ${masterAddress}`;
111
+ return {
112
+ ok: false,
113
+ fleet_state: state,
114
+ message: friendly,
115
+ funding: {
116
+ master_address: masterAddress,
117
+ eth_required: shortfall.toString(),
118
+ eth_balance: masterBalance.toString(),
119
+ },
120
+ };
121
+ }
122
+ this.warnMasterEthRunway(masterAddress, masterBalance, requiredMasterEth);
123
+ // Phase 2: Bootstrap services up to target
124
+ const mnemonic = await decryptMnemonic(await this.store.loadMnemonicKeystore(), password);
125
+ state = await this.reconcileFleetWithChain(state, mnemonic);
126
+ // Resume all services. For incomplete services, this picks up where they
127
+ // left off. For "complete" services in standard mode, this also runs the
128
+ // eviction recovery check (since on-chain state may have changed since
129
+ // the daemon was last running — e.g., evicted due to inactivity).
130
+ for (const svc of state.services) {
131
+ if (svc.step !== 'complete') {
132
+ console.error(`[fleet-bootstrap] Resuming service ${svc.index} at step '${svc.step}'`);
133
+ }
134
+ state = await this.resumeService(state, mnemonic, svc.index);
135
+ }
136
+ // Then create new services if needed
137
+ const completedCount = state.services.filter(s => s.step === 'complete').length;
138
+ const needed = this.targetServices - completedCount;
139
+ if (needed > 0) {
140
+ console.error(`[fleet-bootstrap] ${completedCount}/${this.targetServices} services complete, bootstrapping ${needed} more`);
141
+ }
142
+ for (let i = 0; i < needed; i++) {
143
+ const nextIndex = nextFleetServiceIndex(state.services);
144
+ state = await this.bootstrapService(state, mnemonic, nextIndex);
145
+ }
146
+ return {
147
+ ok: true,
148
+ fleet_state: state,
149
+ message: `Fleet bootstrap complete. ${state.services.filter(s => s.step === 'complete').length}/${this.targetServices} services running.`,
150
+ };
151
+ }
152
+ catch (error) {
153
+ const { summary, hint } = formatBootstrapOperatorMessage(error);
154
+ const userMessage = hint !== undefined ? `${summary}\nHint: ${hint}` : summary;
155
+ if (this.debug) {
156
+ console.error(`[fleet-bootstrap] Bootstrap failed:`, error);
157
+ }
158
+ else {
159
+ console.error(`[fleet-bootstrap] ${summary}`);
160
+ if (hint !== undefined)
161
+ console.error(`Hint: ${hint}`);
162
+ }
163
+ return {
164
+ ok: false,
165
+ fleet_state: state,
166
+ message: userMessage,
167
+ };
168
+ }
169
+ }
170
+ /**
171
+ * If the master is only slightly above the minimum, warn about gas runway (heuristic days).
172
+ */
173
+ warnMasterEthRunway(masterAddress, masterBalance, requiredMasterEth) {
174
+ const daily = this.masterEthDailyEstimateWei;
175
+ if (daily === 0n)
176
+ return;
177
+ const excess = masterBalance > requiredMasterEth ? masterBalance - requiredMasterEth : 0n;
178
+ const threshold = daily * MASTER_ETH_RUNWAY_WARN_DAYS;
179
+ if (excess >= threshold)
180
+ return;
181
+ const days = excess / daily;
182
+ console.error(`[fleet-bootstrap] Warning: Master wallet ETH headroom is low (~${days} day(s) at estimated daily usage). ` +
183
+ `Consider sending more ETH to: ${masterAddress}`);
184
+ }
185
+ // ── Phase 1: Master wallet ───────────────────────────────────────────
186
+ async ensureMasterWallet(state, password) {
187
+ if (this.store.hasMnemonicKeystore() && state.master_address) {
188
+ return state;
189
+ }
190
+ console.error('[fleet-bootstrap] Generating new HD wallet...');
191
+ const mnemonic = generateMnemonic();
192
+ const encrypted = await encryptMnemonic(mnemonic, password);
193
+ await this.store.saveMnemonicKeystore(encrypted);
194
+ const masterAddress = deriveMasterAddress(mnemonic);
195
+ console.error(`[fleet-bootstrap] Master address: ${masterAddress}`);
196
+ return this.store.patchFleet({
197
+ master_address: masterAddress,
198
+ chain: this.chain,
199
+ staking_mode: this.stakingMode,
200
+ });
201
+ }
202
+ // ── Phase 2: Per-service bootstrap ───────────────────────────────────
203
+ async bootstrapService(state, mnemonic, index) {
204
+ const agentAddress = deriveAgentAddress(mnemonic, index);
205
+ const svc = createDefaultServiceState(index, agentAddress);
206
+ console.error(`[fleet-bootstrap] Service ${index}: agent ${agentAddress}`);
207
+ state = await this.store.addService(svc);
208
+ return this.resumeService(state, mnemonic, index);
209
+ }
210
+ /**
211
+ * Compare persisted per-service state to registry/staking/Safe bytecode and patch store
212
+ * when local JSON is ahead, behind, or stale (idempotent; safe to repeat).
213
+ */
214
+ async reconcileFleetWithChain(state, mnemonic) {
215
+ const ctx = { stakingContract: this.config.stakingContract };
216
+ let next = state;
217
+ for (const svc of state.services) {
218
+ const signals = await this.gatherChainSignals(svc);
219
+ const result = reconcileServiceAgainstChain(this.stakingMode, svc, signals, ctx);
220
+ if (result) {
221
+ const abandoned = previousSafeBeingAbandoned(svc, result.patch);
222
+ if (abandoned && state.master_address) {
223
+ await this.sweepAbandonedSafeForService(state, mnemonic, svc.index, abandoned);
224
+ }
225
+ console.error(result.message);
226
+ next = await this.store.updateService(svc.index, result.patch);
227
+ }
228
+ }
229
+ return next;
230
+ }
231
+ /** Best-effort ETH recovery before persisted Safe address is cleared or replaced. */
232
+ async sweepAbandonedSafeForService(state, mnemonic, serviceIndex, abandonedSafeAddress) {
233
+ if (!state.master_address)
234
+ return;
235
+ const masterSigner = deriveMasterSigner(mnemonic);
236
+ const agentSigner = deriveAgentSigner(mnemonic, serviceIndex);
237
+ await sweepOrphanedServiceFunds({
238
+ rpcUrl: this.config.rpcUrl,
239
+ network: this.chain,
240
+ publicClient: this.publicClient,
241
+ masterAddress: state.master_address,
242
+ masterAccount: masterSigner,
243
+ serviceIndex,
244
+ agentPrivateKey: walletPrivateKeyAtIndex(mnemonic, serviceIndex),
245
+ agentAddress: agentSigner.address,
246
+ abandonedSafeAddress,
247
+ minAgentReserveWei: this.config.minEoaGasEth,
248
+ });
249
+ }
250
+ async gatherChainSignals(svc) {
251
+ let safeDeployed = null;
252
+ if (svc.safe_address) {
253
+ try {
254
+ const code = await this.publicClient.getCode({ address: getAddress(svc.safe_address) });
255
+ safeDeployed = code !== '0x';
256
+ }
257
+ catch {
258
+ safeDeployed = null;
259
+ }
260
+ }
261
+ if (svc.service_id === null) {
262
+ return {
263
+ stakingState: 0,
264
+ stakingMultisig: null,
265
+ registryState: 0,
266
+ registryMultisig: null,
267
+ safeDeployed,
268
+ };
269
+ }
270
+ const id = svc.service_id;
271
+ const stakingAddr = this.config.stakingContract;
272
+ const registryAddr = this.config.serviceRegistry;
273
+ let stakingState = 0;
274
+ try {
275
+ stakingState = Number(await this.publicClient.readContract({
276
+ address: stakingAddr,
277
+ abi: STAKING_ABI,
278
+ functionName: 'getStakingState',
279
+ args: [BigInt(id)],
280
+ }));
281
+ }
282
+ catch (e) {
283
+ stakingState = isTransientEthReadError(e) ? 'inconclusive' : 'revert';
284
+ }
285
+ let stakingMultisig = null;
286
+ if (stakingState !== 'revert' && stakingState !== 'inconclusive') {
287
+ try {
288
+ const info = await this.publicClient.readContract({
289
+ address: stakingAddr,
290
+ abi: STAKING_ABI,
291
+ functionName: 'getServiceInfo',
292
+ args: [BigInt(id)],
293
+ });
294
+ const m = info[1];
295
+ stakingMultisig =
296
+ m && getAddress(m) !== getAddress(zeroAddress) ? getAddress(m) : null;
297
+ }
298
+ catch {
299
+ stakingMultisig = null;
300
+ }
301
+ }
302
+ let registryState = 0;
303
+ let registryMultisig = null;
304
+ try {
305
+ const s = await this.publicClient.readContract({
306
+ address: registryAddr,
307
+ abi: SERVICE_REGISTRY_L2_ABI,
308
+ functionName: 'getService',
309
+ args: [BigInt(id)],
310
+ });
311
+ registryState = Number(s.state);
312
+ const m = s.multisig;
313
+ registryMultisig =
314
+ m && getAddress(m) !== getAddress(zeroAddress) ? getAddress(m) : null;
315
+ }
316
+ catch (e) {
317
+ registryState = isTransientEthReadError(e) ? 'inconclusive' : 'revert';
318
+ registryMultisig = null;
319
+ }
320
+ if (stakingState === 'inconclusive' || registryState === 'inconclusive') {
321
+ console.error(`[fleet-bootstrap] Service ${svc.index}: chain read inconclusive (likely RPC). Skipping reconcile for this run; persisted state unchanged.`);
322
+ }
323
+ return {
324
+ stakingState,
325
+ stakingMultisig,
326
+ registryState,
327
+ registryMultisig,
328
+ safeDeployed,
329
+ };
330
+ }
331
+ async resumeService(state, mnemonic, index) {
332
+ let svc = state.services.find(s => s.index === index);
333
+ if (!svc)
334
+ throw new Error(`Service ${index} not found in state`);
335
+ // Eviction recovery: even for "complete" services, check if on-chain shows
336
+ // evicted (state=2). If so, unstake and reset to awaiting_stake so the
337
+ // bootstrap restakes fresh. Only applies to standard mode (distributor-managed).
338
+ if (this.stakingMode === 'standard' &&
339
+ svc.service_id !== null &&
340
+ (svc.step === 'complete' || svc.step === 'mech_deployed' || svc.step === 'staked')) {
341
+ const onChainState = await this.getStakingState(svc.service_id);
342
+ if (onChainState === 2) {
343
+ console.error(`[jinn-earning] Noticed service ${svc.service_id} (fleet index ${index}) evicted on-chain; running distributor reStake to restake.`);
344
+ state = await this.recoverEvictedService(state, mnemonic, index);
345
+ svc = state.services.find(s => s.index === index);
346
+ }
347
+ }
348
+ if (svc.step === 'complete')
349
+ return state;
350
+ if (this.stakingMode === 'standard') {
351
+ return this.resumeServiceStandard(state, mnemonic, index);
352
+ }
353
+ return this.resumeServiceSelfBond(state, mnemonic, index);
354
+ }
355
+ async resumeServiceStandard(state, mnemonic, index) {
356
+ const svc = state.services.find(s => s.index === index);
357
+ if (svc.step === 'awaiting_stake') {
358
+ state = await this.stepStolasStake(state, mnemonic, index);
359
+ }
360
+ // Reload service state after stake
361
+ const updatedSvc = (await this.store.load(this.chain)).services.find(s => s.index === index);
362
+ if (!updatedSvc)
363
+ throw new Error(`Service ${index} disappeared from state`);
364
+ if (updatedSvc.step === 'staked' || updatedSvc.step === 'mech_deployed') {
365
+ state = await this.stepDeployMech(state, mnemonic, index);
366
+ }
367
+ return this.store.load(this.chain);
368
+ }
369
+ async resumeServiceSelfBond(state, mnemonic, index) {
370
+ let svc = state.services.find(s => s.index === index);
371
+ if (svc.step === 'awaiting_stake') {
372
+ state = await this.stepSelfBondSetup(state, mnemonic, index);
373
+ svc = (await this.store.load(this.chain)).services.find(s => s.index === index);
374
+ }
375
+ if (svc.step === 'service_created') {
376
+ state = await this.stepSelfBondCreateService(state, mnemonic, index);
377
+ svc = (await this.store.load(this.chain)).services.find(s => s.index === index);
378
+ }
379
+ if (svc.step === 'service_activated') {
380
+ state = await this.stepSelfBondActivateService(state, mnemonic, index);
381
+ svc = (await this.store.load(this.chain)).services.find(s => s.index === index);
382
+ }
383
+ if (svc.step === 'agents_registered') {
384
+ state = await this.stepSelfBondRegisterAgents(state, mnemonic, index);
385
+ svc = (await this.store.load(this.chain)).services.find(s => s.index === index);
386
+ }
387
+ if (svc.step === 'service_deployed') {
388
+ state = await this.stepSelfBondDeployService(state, mnemonic, index);
389
+ svc = (await this.store.load(this.chain)).services.find(s => s.index === index);
390
+ }
391
+ if (svc.step === 'service_staked') {
392
+ state = await this.stepSelfBondStakeService(state, mnemonic, index);
393
+ svc = (await this.store.load(this.chain)).services.find(s => s.index === index);
394
+ }
395
+ if (svc.step === 'staked' || svc.step === 'mech_deployed') {
396
+ state = await this.stepDeployMech(state, mnemonic, index);
397
+ }
398
+ return this.store.load(this.chain);
399
+ }
400
+ async stepStolasStake(state, mnemonic, index) {
401
+ const svc = state.services.find(s => s.index === index);
402
+ // Idempotency: if this service already has an id and is already staked, skip
403
+ if (svc.service_id !== null) {
404
+ const stakingState = await this.getStakingState(svc.service_id);
405
+ if (stakingState === 1) {
406
+ console.error(`[fleet-bootstrap] Service ${index} already staked, skipping`);
407
+ return this.store.updateService(index, { step: 'staked' });
408
+ }
409
+ }
410
+ // Fresh distributor stake() creates a new on-chain service. If state still
411
+ // references an old Safe (e.g. hand-edited JSON), sweep it before replacing.
412
+ if (svc.service_id === null && svc.safe_address && state.master_address) {
413
+ try {
414
+ const oldSafe = getAddress(svc.safe_address);
415
+ await this.sweepAbandonedSafeForService(state, mnemonic, index, oldSafe);
416
+ }
417
+ catch {
418
+ // Ignore invalid persisted safe_address; later steps will surface errors if needed.
419
+ }
420
+ }
421
+ // Preflight
422
+ await this.stolasPreflightCheck();
423
+ // Master EOA signs the stake() call
424
+ const masterAccount = deriveMasterSigner(mnemonic);
425
+ const masterWallet = createJinnWalletClient(this.config.rpcUrl, this.chain, masterAccount);
426
+ const agentAddress = svc.agent_address;
427
+ const configHashBytes = cidToBytes32(this.config.serviceHash);
428
+ const stakeData = encodeFunctionData({
429
+ abi: STOLAS_DISTRIBUTOR_ABI,
430
+ functionName: 'stake',
431
+ args: [
432
+ this.config.stakingContract,
433
+ 0n,
434
+ BigInt(this.config.agentId),
435
+ configHashBytes,
436
+ agentAddress,
437
+ ],
438
+ });
439
+ console.error(`[fleet-bootstrap] Service ${index}: calling distributor.stake() from master`);
440
+ const txHash = await viemSendTransactionWithRetry(masterWallet, this.publicClient, {
441
+ account: masterAccount,
442
+ to: addr(this.config.distributorAddress),
443
+ data: stakeData,
444
+ gas: 2500000n,
445
+ });
446
+ const receipt = await waitForTransactionReceiptWithRetry(this.publicClient, txHash);
447
+ if (receipt.status !== 'success') {
448
+ throw new Error(`stOLAS stake() tx failed for service ${index}: ${txHash}`);
449
+ }
450
+ console.error(`[fleet-bootstrap] Service ${index}: stake() confirmed (tx: ${txHash})`);
451
+ // Parse events
452
+ const serviceId = await this.parseServiceIdFromReceipt(receipt);
453
+ if (serviceId === null) {
454
+ throw new Error(`stake() succeeded but CreateService event not found (tx: ${txHash})`);
455
+ }
456
+ const safeAddress = this.parseMultisigFromReceipt(receipt);
457
+ if (!safeAddress) {
458
+ throw new Error(`stake() succeeded but CreateMultisigWithAgents event not found (tx: ${txHash})`);
459
+ }
460
+ console.error(`[fleet-bootstrap] Service ${index}: id=${serviceId}, safe=${safeAddress}`);
461
+ return this.store.updateService(index, {
462
+ service_id: serviceId,
463
+ safe_address: safeAddress,
464
+ staking_address: this.config.stakingContract,
465
+ step: 'staked',
466
+ });
467
+ }
468
+ async recoverEvictedService(state, mnemonic, index) {
469
+ if (!this.config.distributorAddress) {
470
+ throw new Error('distributorAddress not configured');
471
+ }
472
+ const svc = state.services.find(s => s.index === index);
473
+ const serviceId = svc.service_id;
474
+ // Master EOA is the curating agent for this service and pays gas
475
+ const masterAccount = deriveMasterSigner(mnemonic);
476
+ const masterWallet = createJinnWalletClient(this.config.rpcUrl, this.chain, masterAccount);
477
+ // Use distributor.reStake() — a purpose-built entry point for evicted services.
478
+ // It calls IStaking.unstake() → IStaking.stake() on the staking proxy without
479
+ // touching the service lifecycle (no terminate/unbond/recoverAccess). The service
480
+ // stays in Deployed state, the Safe owners are untouched, and the same service
481
+ // ID, Safe address, and mech address are preserved across the eviction.
482
+ // Authorization: master EOA is a curating agent (recorded when it called stake()).
483
+ const reStakeData = encodeFunctionData({
484
+ abi: STOLAS_DISTRIBUTOR_ABI,
485
+ functionName: 'reStake',
486
+ args: [this.config.stakingContract, BigInt(serviceId)],
487
+ });
488
+ console.error(`[fleet-bootstrap] Service ${index}: calling distributor.reStake() for evicted service ${serviceId}`);
489
+ const reStakeHash = await viemSendTransactionWithRetry(masterWallet, this.publicClient, {
490
+ account: masterAccount,
491
+ to: addr(this.config.distributorAddress),
492
+ data: reStakeData,
493
+ gas: 1500000n,
494
+ });
495
+ const receipt = await waitForTransactionReceiptWithRetry(this.publicClient, reStakeHash);
496
+ if (receipt.status !== 'success') {
497
+ throw new Error(`reStake failed for service ${index}: ${reStakeHash}`);
498
+ }
499
+ console.error(`[fleet-bootstrap] Service ${index}: reStake confirmed (tx: ${reStakeHash})`);
500
+ // Service is now Staked again with the same service_id, safe_address, and mech_address.
501
+ // Mark the step as complete so the bootstrap doesn't try to re-stake or re-deploy mech.
502
+ return this.store.updateService(index, {
503
+ step: 'complete',
504
+ });
505
+ }
506
+ async stepDeployMech(state, mnemonic, index) {
507
+ const svc = state.services.find(s => s.index === index);
508
+ if (svc.mech_address) {
509
+ console.error(`[fleet-bootstrap] Service ${index}: mech already deployed at ${svc.mech_address}`);
510
+ return this.store.updateService(index, { step: 'complete' });
511
+ }
512
+ const serviceId = svc.service_id;
513
+ const safeAddress = svc.safe_address;
514
+ // Fund agent with gas from master
515
+ const masterAccount = deriveMasterSigner(mnemonic);
516
+ const masterWallet = createJinnWalletClient(this.config.rpcUrl, this.chain, masterAccount);
517
+ const agentBalance = await this.publicClient.getBalance({
518
+ address: getAddress(svc.agent_address),
519
+ });
520
+ // Agent needs enough gas for mech deployment Safe tx (~2.6M gas)
521
+ const minAgentGas = this.config.minEoaGasEth;
522
+ if (agentBalance < minAgentGas) {
523
+ const fundAmount = minAgentGas - agentBalance;
524
+ console.error(`[fleet-bootstrap] Service ${index}: funding agent with ${fundAmount} wei`);
525
+ const fundHash = await viemSendTransactionWithRetry(masterWallet, this.publicClient, {
526
+ account: masterAccount,
527
+ to: addr(svc.agent_address),
528
+ value: fundAmount,
529
+ });
530
+ await waitForTransactionReceiptWithRetry(this.publicClient, fundHash);
531
+ }
532
+ // Deploy mech via the service Safe (agent is Safe owner)
533
+ const agentKey = walletPrivateKeyAtIndex(mnemonic, index);
534
+ const safe = await initDeployedSafe({
535
+ rpcUrl: this.config.rpcUrl,
536
+ signerKey: agentKey,
537
+ safeAddress,
538
+ });
539
+ const payload = encodeAbiParameters([{ type: 'uint256' }], [this.config.mechRequestPrice]);
540
+ const createData = encodeFunctionData({
541
+ abi: MECH_MARKETPLACE_CREATE_ABI,
542
+ functionName: 'create',
543
+ args: [BigInt(serviceId), this.config.mechFactory, payload],
544
+ });
545
+ console.error(`[fleet-bootstrap] Service ${index}: deploying mech`);
546
+ const result = await executeSafeTxBatch(safe, [
547
+ { to: this.config.mechMarketplace, value: '0', data: createData },
548
+ ]);
549
+ const mechReceipt = await waitForTransactionReceiptWithRetry(this.publicClient, result.hash);
550
+ if (mechReceipt.status !== 'success') {
551
+ throw new Error(`Mech deployment tx failed for service ${index}: ${result.hash}`);
552
+ }
553
+ // Parse CreateMech event
554
+ const createMechTopic = '0x46e1ca45c09520471c43e2e88eca33bb51803011cfd456933629dcc645ecacd6';
555
+ let mechAddress = null;
556
+ for (const log of mechReceipt.logs) {
557
+ const t0 = log.topics[0];
558
+ if (t0 === createMechTopic && log.topics.length >= 2) {
559
+ mechAddress = getAddress(('0x' + log.topics[1].slice(26)));
560
+ break;
561
+ }
562
+ }
563
+ if (!mechAddress) {
564
+ throw new Error(`CreateMech event not found for service ${index} (tx: ${result.hash})`);
565
+ }
566
+ console.error(`[fleet-bootstrap] Service ${index}: mech deployed at ${mechAddress}`);
567
+ return this.store.updateService(index, {
568
+ mech_address: mechAddress,
569
+ step: 'complete',
570
+ });
571
+ }
572
+ // ── Self-bond step handlers ──────────────────────────────────────────
573
+ async stepSelfBondSetup(state, mnemonic, index) {
574
+ const svc = state.services.find(s => s.index === index);
575
+ const agentSigner = deriveAgentSigner(mnemonic, index);
576
+ const agentKey = walletPrivateKeyAtIndex(mnemonic, index);
577
+ const agentAddress = svc.agent_address;
578
+ // 1. Predict Safe if not yet done
579
+ if (!svc.safe_address) {
580
+ console.error(`[fleet-bootstrap] Service ${index}: predicting Safe for agent ${agentAddress}`);
581
+ const { address } = await initPredictedSafe({
582
+ rpcUrl: this.config.rpcUrl,
583
+ signerKey: agentKey,
584
+ owners: [agentAddress],
585
+ threshold: 1,
586
+ });
587
+ state = await this.store.updateService(index, { safe_address: getAddress(address) });
588
+ }
589
+ // Reload svc to get safe_address
590
+ const updatedSvc = (await this.store.load(this.chain)).services.find(s => s.index === index);
591
+ const safeAddress = updatedSvc.safe_address;
592
+ // 2. Fund agent EOA from master if needed
593
+ // The agent pays for: Safe deploy + Safe top-up + ~8 Safe txs (service lifecycle + staking + mech)
594
+ const SELF_BOND_AGENT_ETH = 25000000000000000n; // 0.025 ETH
595
+ const requiredAgentEth = SELF_BOND_AGENT_ETH;
596
+ const masterAccount = deriveMasterSigner(mnemonic);
597
+ const masterWallet = createJinnWalletClient(this.config.rpcUrl, this.chain, masterAccount);
598
+ const agentBalance = await this.publicClient.getBalance({ address: getAddress(agentAddress) });
599
+ if (agentBalance < requiredAgentEth) {
600
+ const fundAmount = requiredAgentEth - agentBalance;
601
+ console.error(`[fleet-bootstrap] Service ${index}: funding agent with ${fundAmount} wei from master`);
602
+ const fundHash = await viemSendTransactionWithRetry(masterWallet, this.publicClient, {
603
+ account: masterAccount,
604
+ to: addr(agentAddress),
605
+ value: fundAmount,
606
+ });
607
+ await waitForTransactionReceiptWithRetry(this.publicClient, fundHash);
608
+ }
609
+ // 3. Check agent ETH balance (retry — public RPCs can lag after a write)
610
+ let agentBalanceAfter = 0n;
611
+ for (let attempt = 0; attempt < 5; attempt++) {
612
+ agentBalanceAfter = await this.publicClient.getBalance({ address: getAddress(agentAddress) });
613
+ if (agentBalanceAfter >= requiredAgentEth)
614
+ break;
615
+ if (attempt < 4)
616
+ await new Promise(r => setTimeout(r, 2000));
617
+ }
618
+ if (agentBalanceAfter < requiredAgentEth) {
619
+ throw new Error(`Service ${index}: agent ${agentAddress} needs ${requiredAgentEth} wei ETH but has ${agentBalanceAfter}`);
620
+ }
621
+ // 4. Check Safe ETH balance (agent can auto-top)
622
+ let safeEthBalance = await this.publicClient.getBalance({ address: getAddress(safeAddress) });
623
+ if (safeEthBalance < this.config.minSafeEth) {
624
+ const eoaAvailable = agentBalanceAfter - this.config.minEoaGasEth;
625
+ const shortfall = this.config.minSafeEth - safeEthBalance;
626
+ if (eoaAvailable >= shortfall) {
627
+ console.error(`[fleet-bootstrap] Service ${index}: auto-topping Safe with ${shortfall} wei ETH`);
628
+ const agentWallet = createJinnWalletClient(this.config.rpcUrl, this.chain, agentSigner);
629
+ const topHash = await viemSendTransactionWithRetry(agentWallet, this.publicClient, {
630
+ account: agentSigner,
631
+ to: addr(safeAddress),
632
+ value: shortfall,
633
+ });
634
+ await waitForTransactionReceiptWithRetry(this.publicClient, topHash);
635
+ safeEthBalance += shortfall;
636
+ }
637
+ }
638
+ if (safeEthBalance < this.config.minSafeEth) {
639
+ throw new Error(`Service ${index}: Safe ${safeAddress} needs ${this.config.minSafeEth} wei ETH but has ${safeEthBalance}`);
640
+ }
641
+ // 5. Check Safe OLAS balance
642
+ const requiredOlas = this.config.bondAmount * SAFE_TOKEN_BOOTSTRAP_MULTIPLIER;
643
+ const olasBalance = await this.getOlasBalance(safeAddress);
644
+ if (olasBalance < requiredOlas) {
645
+ throw new Error(`Service ${index}: Safe ${safeAddress} needs ${requiredOlas} OLAS wei for bonding but has ${olasBalance}. ` +
646
+ `Send OLAS tokens to the Safe address.`);
647
+ }
648
+ // 6. Deploy Safe if not yet deployed
649
+ const code = await this.publicClient.getCode({ address: getAddress(safeAddress) });
650
+ if (code === undefined || code === '0x') {
651
+ console.error(`[fleet-bootstrap] Service ${index}: deploying Safe at ${safeAddress}`);
652
+ const { safe } = await initPredictedSafe({
653
+ rpcUrl: this.config.rpcUrl,
654
+ signerKey: agentKey,
655
+ owners: [agentAddress],
656
+ threshold: 1,
657
+ });
658
+ const deployTx = await safe.createSafeDeploymentTransaction();
659
+ const agentWallet = createJinnWalletClient(this.config.rpcUrl, this.chain, agentSigner);
660
+ const deployHash = await viemSendTransactionWithRetry(agentWallet, this.publicClient, {
661
+ account: agentSigner,
662
+ to: deployTx.to,
663
+ value: BigInt(deployTx.value),
664
+ data: deployTx.data,
665
+ });
666
+ const receipt = await waitForTransactionReceiptWithRetry(this.publicClient, deployHash);
667
+ if (receipt.status !== 'success') {
668
+ throw new Error(`Safe deployment tx failed for service ${index}: ${deployHash}`);
669
+ }
670
+ const deployedCode = await this.publicClient.getCode({ address: getAddress(safeAddress) });
671
+ if (deployedCode === undefined || deployedCode === '0x') {
672
+ throw new Error(`Safe deployment succeeded but no code at ${safeAddress}`);
673
+ }
674
+ console.error(`[fleet-bootstrap] Service ${index}: Safe deployed (tx: ${deployHash})`);
675
+ }
676
+ // 7. Advance to service_created
677
+ return this.store.updateService(index, { step: 'service_created' });
678
+ }
679
+ async stepSelfBondCreateService(state, mnemonic, index) {
680
+ const svc = state.services.find(s => s.index === index);
681
+ // Idempotency: if service already exists on-chain, skip
682
+ if (svc.service_id !== null) {
683
+ const onChainState = await this.getServiceState(svc.service_id);
684
+ if (onChainState >= 1) { // PreRegistration or beyond
685
+ console.error(`[fleet-bootstrap] Service ${index}: service ${svc.service_id} already created, skipping`);
686
+ return this.store.updateService(index, { step: 'service_activated' });
687
+ }
688
+ }
689
+ const agentKey = walletPrivateKeyAtIndex(mnemonic, index);
690
+ const safeAddress = svc.safe_address;
691
+ const safe = await initDeployedSafe({
692
+ rpcUrl: this.config.rpcUrl,
693
+ signerKey: agentKey,
694
+ safeAddress,
695
+ });
696
+ const configHashBytes = cidToBytes32(this.config.serviceHash);
697
+ const createData = encodeFunctionData({
698
+ abi: SERVICE_MANAGER_ABI,
699
+ functionName: 'create',
700
+ args: [
701
+ getAddress(safeAddress),
702
+ this.config.olasToken,
703
+ configHashBytes,
704
+ [this.config.agentId],
705
+ [{ slots: 1, bond: this.config.bondAmount }],
706
+ 1,
707
+ ],
708
+ });
709
+ console.error(`[fleet-bootstrap] Service ${index}: creating service through Safe`);
710
+ const result = await executeSafeTxBatch(safe, [
711
+ { to: this.config.serviceManager, value: '0', data: createData },
712
+ ]);
713
+ const receipt = await waitForTransactionReceiptWithRetry(this.publicClient, result.hash);
714
+ if (receipt.status !== 'success') {
715
+ throw new Error(`Create service tx failed for service ${index}: ${result.hash}`);
716
+ }
717
+ const serviceId = await this.parseServiceIdFromReceipt(receipt);
718
+ if (serviceId === null) {
719
+ throw new Error(`CreateService event not found for service ${index} (tx: ${result.hash})`);
720
+ }
721
+ console.error(`[fleet-bootstrap] Service ${index}: created id=${serviceId} (tx: ${result.hash})`);
722
+ return this.store.updateService(index, {
723
+ service_id: serviceId,
724
+ staking_address: this.config.stakingContract,
725
+ step: 'service_activated',
726
+ });
727
+ }
728
+ async stepSelfBondActivateService(state, mnemonic, index) {
729
+ const svc = state.services.find(s => s.index === index);
730
+ const serviceId = svc.service_id;
731
+ // Idempotency
732
+ const onChainState = await this.getServiceState(serviceId);
733
+ if (onChainState >= 2) { // ActiveRegistration or beyond
734
+ console.error(`[fleet-bootstrap] Service ${index}: service ${serviceId} already activated, skipping`);
735
+ return this.store.updateService(index, { step: 'agents_registered' });
736
+ }
737
+ const agentKey = walletPrivateKeyAtIndex(mnemonic, index);
738
+ const safe = await initDeployedSafe({
739
+ rpcUrl: this.config.rpcUrl,
740
+ signerKey: agentKey,
741
+ safeAddress: svc.safe_address,
742
+ });
743
+ const approveData = encodeFunctionData({
744
+ abi: ERC20_ABI,
745
+ functionName: 'approve',
746
+ args: [this.config.serviceRegistryTokenUtility, this.config.bondAmount],
747
+ });
748
+ const activateData = encodeFunctionData({
749
+ abi: SERVICE_MANAGER_ABI,
750
+ functionName: 'activateRegistration',
751
+ args: [BigInt(serviceId)],
752
+ });
753
+ console.error(`[fleet-bootstrap] Service ${index}: activating service ${serviceId}`);
754
+ const result = await executeSafeTxBatch(safe, [
755
+ { to: this.config.olasToken, value: '0', data: approveData },
756
+ { to: this.config.serviceManager, value: '1', data: activateData },
757
+ ]);
758
+ const receipt = await waitForTransactionReceiptWithRetry(this.publicClient, result.hash);
759
+ if (receipt.status !== 'success') {
760
+ throw new Error(`Activate service tx failed for service ${index}: ${result.hash}`);
761
+ }
762
+ console.error(`[fleet-bootstrap] Service ${index}: activated (tx: ${result.hash})`);
763
+ return this.store.updateService(index, { step: 'agents_registered' });
764
+ }
765
+ async stepSelfBondRegisterAgents(state, mnemonic, index) {
766
+ const svc = state.services.find(s => s.index === index);
767
+ const serviceId = svc.service_id;
768
+ // Idempotency
769
+ const onChainState = await this.getServiceState(serviceId);
770
+ if (onChainState >= 3) { // FinishedRegistration or beyond
771
+ console.error(`[fleet-bootstrap] Service ${index}: agents already registered for service ${serviceId}, skipping`);
772
+ return this.store.updateService(index, { step: 'service_deployed' });
773
+ }
774
+ const agentKey = walletPrivateKeyAtIndex(mnemonic, index);
775
+ const safe = await initDeployedSafe({
776
+ rpcUrl: this.config.rpcUrl,
777
+ signerKey: agentKey,
778
+ safeAddress: svc.safe_address,
779
+ });
780
+ const approveData = encodeFunctionData({
781
+ abi: ERC20_ABI,
782
+ functionName: 'approve',
783
+ args: [this.config.serviceRegistryTokenUtility, this.config.bondAmount],
784
+ });
785
+ const registerData = encodeFunctionData({
786
+ abi: SERVICE_MANAGER_ABI,
787
+ functionName: 'registerAgents',
788
+ args: [BigInt(serviceId), [getAddress(svc.agent_address)], [this.config.agentId]],
789
+ });
790
+ console.error(`[fleet-bootstrap] Service ${index}: registering agent ${svc.agent_address} for service ${serviceId}`);
791
+ const result = await executeSafeTxBatch(safe, [
792
+ { to: this.config.olasToken, value: '0', data: approveData },
793
+ { to: this.config.serviceManager, value: '1', data: registerData },
794
+ ]);
795
+ const receipt = await waitForTransactionReceiptWithRetry(this.publicClient, result.hash);
796
+ if (receipt.status !== 'success') {
797
+ throw new Error(`Register agents tx failed for service ${index}: ${result.hash}`);
798
+ }
799
+ console.error(`[fleet-bootstrap] Service ${index}: agents registered (tx: ${result.hash})`);
800
+ return this.store.updateService(index, { step: 'service_deployed' });
801
+ }
802
+ async stepSelfBondDeployService(state, mnemonic, index) {
803
+ const svc = state.services.find(s => s.index === index);
804
+ const serviceId = svc.service_id;
805
+ // Idempotency
806
+ const onChainState = await this.getServiceState(serviceId);
807
+ if (onChainState >= 4) { // Deployed or beyond
808
+ console.error(`[fleet-bootstrap] Service ${index}: service ${serviceId} already deployed, skipping`);
809
+ return this.store.updateService(index, { step: 'service_staked' });
810
+ }
811
+ const agentKey = walletPrivateKeyAtIndex(mnemonic, index);
812
+ const safeAddress = svc.safe_address;
813
+ const safe = await initDeployedSafe({
814
+ rpcUrl: this.config.rpcUrl,
815
+ signerKey: agentKey,
816
+ safeAddress,
817
+ });
818
+ const multisigInitBytes = encodeAbiParameters([{ type: 'address' }], [addr(safeAddress)]);
819
+ const deployData = encodeFunctionData({
820
+ abi: SERVICE_MANAGER_ABI,
821
+ functionName: 'deploy',
822
+ args: [
823
+ BigInt(serviceId),
824
+ addr(this.config.gnosisSafeSameAddressMultisig),
825
+ multisigInitBytes,
826
+ ],
827
+ });
828
+ console.error(`[fleet-bootstrap] Service ${index}: deploying service ${serviceId}`);
829
+ const result = await executeSafeTxBatch(safe, [
830
+ { to: this.config.serviceManager, value: '0', data: deployData },
831
+ ]);
832
+ const receipt = await waitForTransactionReceiptWithRetry(this.publicClient, result.hash);
833
+ if (receipt.status !== 'success') {
834
+ throw new Error(`Deploy service tx failed for service ${index}: ${result.hash}`);
835
+ }
836
+ console.error(`[fleet-bootstrap] Service ${index}: service deployed (tx: ${result.hash})`);
837
+ return this.store.updateService(index, { step: 'service_staked' });
838
+ }
839
+ async stepSelfBondStakeService(state, mnemonic, index) {
840
+ const svc = state.services.find(s => s.index === index);
841
+ const serviceId = svc.service_id;
842
+ // Idempotency: check if already staked
843
+ const stakingState = await this.getStakingState(serviceId);
844
+ if (stakingState === 1) {
845
+ console.error(`[fleet-bootstrap] Service ${index}: service ${serviceId} already staked, skipping`);
846
+ return this.store.updateService(index, { step: 'staked' });
847
+ }
848
+ const agentKey = walletPrivateKeyAtIndex(mnemonic, index);
849
+ const safeAddress = svc.safe_address;
850
+ const safe = await initDeployedSafe({
851
+ rpcUrl: this.config.rpcUrl,
852
+ signerKey: agentKey,
853
+ safeAddress,
854
+ });
855
+ // Transaction 1: Approve service NFT for staking contract
856
+ const approveData = encodeFunctionData({
857
+ abi: SERVICE_REGISTRY_APPROVE_ABI,
858
+ functionName: 'approve',
859
+ args: [this.config.stakingContract, BigInt(serviceId)],
860
+ });
861
+ console.error(`[fleet-bootstrap] Service ${index}: approving service ${serviceId} NFT for staking`);
862
+ const approveResult = await executeSafeTxBatch(safe, [
863
+ { to: this.config.serviceRegistry, value: '0', data: approveData },
864
+ ]);
865
+ await this.waitForSuccessfulTx(approveResult.hash, `approve service ${serviceId} NFT`);
866
+ console.error(`[fleet-bootstrap] Service ${index}: approve tx confirmed (${approveResult.hash})`);
867
+ // Transaction 2: Stake via executeSafeTxDirect (bypasses Safe SDK gas estimation)
868
+ const stakeData = encodeFunctionData({
869
+ abi: STAKING_ABI,
870
+ functionName: 'stake',
871
+ args: [BigInt(serviceId)],
872
+ });
873
+ console.error(`[fleet-bootstrap] Service ${index}: staking service ${serviceId}`);
874
+ const stakeResult = await executeSafeTxDirect({
875
+ rpcUrl: this.config.rpcUrl,
876
+ signerKey: agentKey,
877
+ safeAddress,
878
+ to: this.config.stakingContract,
879
+ data: stakeData,
880
+ });
881
+ await this.waitForSuccessfulTx(stakeResult.hash, `stake service ${serviceId}`);
882
+ // Verify staking state
883
+ const finalState = await this.getStakingState(serviceId);
884
+ if (finalState !== 1) {
885
+ throw new Error(`Service ${index}: staking verification failed for service ${serviceId}: expected state 1 (Staked) but got ${finalState}`);
886
+ }
887
+ console.error(`[fleet-bootstrap] Service ${index}: service ${serviceId} staked and verified`);
888
+ return this.store.updateService(index, { step: 'staked' });
889
+ }
890
+ // ── Helpers ──────────────────────────────────────────────────────────
891
+ async getOlasBalance(address) {
892
+ return this.publicClient.readContract({
893
+ address: this.config.olasToken,
894
+ abi: ERC20_ABI,
895
+ functionName: 'balanceOf',
896
+ args: [getAddress(address)],
897
+ });
898
+ }
899
+ async getServiceState(serviceId) {
900
+ const service = await this.publicClient.readContract({
901
+ address: this.config.serviceRegistry,
902
+ abi: SERVICE_REGISTRY_L2_ABI,
903
+ functionName: 'getService',
904
+ args: [BigInt(serviceId)],
905
+ });
906
+ return Number(service.state);
907
+ }
908
+ async waitForSuccessfulTx(txHash, label) {
909
+ const receipt = await waitForTransactionReceiptWithRetry(this.publicClient, txHash);
910
+ if (receipt.status !== 'success')
911
+ throw new Error(`${label} tx reverted: ${txHash}`);
912
+ }
913
+ async stolasPreflightCheck() {
914
+ if (!this.config.distributorAddress) {
915
+ throw new Error('distributorAddress not configured. Set JINN_TESTNET_STOLAS_DEPLOYMENT or use stakingMode: self-bond.');
916
+ }
917
+ const proxyConfig = await this.publicClient.readContract({
918
+ address: this.config.distributorAddress,
919
+ abi: STOLAS_DISTRIBUTOR_ABI,
920
+ functionName: 'mapStakingProxyConfigs',
921
+ args: [this.config.stakingContract],
922
+ });
923
+ if (proxyConfig === 0n) {
924
+ throw new Error(`stOLAS distributor not configured for ${this.config.stakingContract}. ` +
925
+ `Use stakingMode: 'self-bond' or contact the stOLAS team.`);
926
+ }
927
+ const serviceIds = await this.publicClient.readContract({
928
+ address: this.config.stakingContract,
929
+ abi: STOLAS_STAKING_SLOTS_ABI,
930
+ functionName: 'getServiceIds',
931
+ });
932
+ const maxServices = await this.publicClient.readContract({
933
+ address: this.config.stakingContract,
934
+ abi: STOLAS_STAKING_SLOTS_ABI,
935
+ functionName: 'maxNumServices',
936
+ });
937
+ const slotsRemaining = Number(maxServices) - serviceIds.length;
938
+ if (slotsRemaining <= 0) {
939
+ throw new Error(`All ${maxServices} staking slots occupied. Try again later.`);
940
+ }
941
+ console.error(`[fleet-bootstrap] Preflight passed: ${slotsRemaining} slots remaining`);
942
+ }
943
+ async getStakingState(serviceId) {
944
+ return Number(await this.publicClient.readContract({
945
+ address: this.config.stakingContract,
946
+ abi: STAKING_ABI,
947
+ functionName: 'getStakingState',
948
+ args: [BigInt(serviceId)],
949
+ }));
950
+ }
951
+ async parseServiceIdFromReceipt(receipt) {
952
+ const createServiceTopic = EVENT_TOPICS.CreateService;
953
+ const serviceRegistryAddress = this.config.serviceRegistry.toLowerCase();
954
+ for (const log of receipt.logs) {
955
+ if (log.address.toLowerCase() !== serviceRegistryAddress ||
956
+ log.topics[0] !== createServiceTopic) {
957
+ continue;
958
+ }
959
+ try {
960
+ const decoded = decodeEventLog({
961
+ abi: SERVICE_REGISTRY_L2_ABI,
962
+ data: log.data,
963
+ topics: log.topics,
964
+ strict: false,
965
+ });
966
+ if (decoded.eventName === 'CreateService' && 'serviceId' in decoded.args) {
967
+ return Number(decoded.args.serviceId);
968
+ }
969
+ }
970
+ catch {
971
+ // Not a matching event
972
+ }
973
+ }
974
+ return null;
975
+ }
976
+ parseMultisigFromReceipt(receipt) {
977
+ const topic = EVENT_TOPICS.CreateMultisigWithAgents;
978
+ for (const log of receipt.logs) {
979
+ const t0 = log.topics[0];
980
+ if (t0 === topic && log.topics.length >= 3) {
981
+ return getAddress(('0x' + log.topics[2].slice(26)));
982
+ }
983
+ }
984
+ return null;
985
+ }
986
+ }
987
+ /** @deprecated Use FleetBootstrapper */
988
+ export const EarningBootstrapper = FleetBootstrapper;
989
+ //# sourceMappingURL=bootstrap.js.map