@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.
- package/LICENSE +21 -0
- package/README.md +162 -0
- package/deployments/deployment-phase1a-l2-baseSepolia-fast.json +32 -0
- package/deployments/deployment-phase1a-token-baseSepolia-fast.json +27 -0
- package/deployments/deployment-phase1b-mech-baseSepolia-fast.json +26 -0
- package/deployments/deployment-stolas-l2-baseSepolia-fast.json +35 -0
- package/dist/adapters/adapter.d.ts +11 -0
- package/dist/adapters/adapter.js +2 -0
- package/dist/adapters/adapter.js.map +1 -0
- package/dist/adapters/local/adapter.d.ts +20 -0
- package/dist/adapters/local/adapter.js +146 -0
- package/dist/adapters/local/adapter.js.map +1 -0
- package/dist/adapters/mech/adapter.d.ts +29 -0
- package/dist/adapters/mech/adapter.js +332 -0
- package/dist/adapters/mech/adapter.js.map +1 -0
- package/dist/adapters/mech/claim-policy.d.ts +40 -0
- package/dist/adapters/mech/claim-policy.js +104 -0
- package/dist/adapters/mech/claim-policy.js.map +1 -0
- package/dist/adapters/mech/contracts.d.ts +44 -0
- package/dist/adapters/mech/contracts.js +323 -0
- package/dist/adapters/mech/contracts.js.map +1 -0
- package/dist/adapters/mech/ipfs.d.ts +43 -0
- package/dist/adapters/mech/ipfs.js +142 -0
- package/dist/adapters/mech/ipfs.js.map +1 -0
- package/dist/adapters/mech/safe.d.ts +15 -0
- package/dist/adapters/mech/safe.js +113 -0
- package/dist/adapters/mech/safe.js.map +1 -0
- package/dist/adapters/mech/types.d.ts +561 -0
- package/dist/adapters/mech/types.js +340 -0
- package/dist/adapters/mech/types.js.map +1 -0
- package/dist/api/balance-build.d.ts +22 -0
- package/dist/api/balance-build.js +37 -0
- package/dist/api/balance-build.js.map +1 -0
- package/dist/api/fleet-build.d.ts +62 -0
- package/dist/api/fleet-build.js +91 -0
- package/dist/api/fleet-build.js.map +1 -0
- package/dist/api/gather-status.d.ts +20 -0
- package/dist/api/gather-status.js +137 -0
- package/dist/api/gather-status.js.map +1 -0
- package/dist/api/history-build.d.ts +32 -0
- package/dist/api/history-build.js +48 -0
- package/dist/api/history-build.js.map +1 -0
- package/dist/api/peers.d.ts +27 -0
- package/dist/api/peers.js +94 -0
- package/dist/api/peers.js.map +1 -0
- package/dist/api/rewards-build.d.ts +20 -0
- package/dist/api/rewards-build.js +42 -0
- package/dist/api/rewards-build.js.map +1 -0
- package/dist/api/server.d.ts +34 -0
- package/dist/api/server.js +130 -0
- package/dist/api/server.js.map +1 -0
- package/dist/api/status-build.d.ts +92 -0
- package/dist/api/status-build.js +175 -0
- package/dist/api/status-build.js.map +1 -0
- package/dist/api/status-rollup-build.d.ts +36 -0
- package/dist/api/status-rollup-build.js +69 -0
- package/dist/api/status-rollup-build.js.map +1 -0
- package/dist/auth/erc8128.d.ts +43 -0
- package/dist/auth/erc8128.js +88 -0
- package/dist/auth/erc8128.js.map +1 -0
- package/dist/bin/jinn.d.ts +11 -0
- package/dist/bin/jinn.js +20 -0
- package/dist/bin/jinn.js.map +1 -0
- package/dist/build-meta.json +3 -0
- package/dist/chain-read-errors.d.ts +9 -0
- package/dist/chain-read-errors.js +43 -0
- package/dist/chain-read-errors.js.map +1 -0
- package/dist/cli/action.d.ts +26 -0
- package/dist/cli/action.js +56 -0
- package/dist/cli/action.js.map +1 -0
- package/dist/cli/command.d.ts +62 -0
- package/dist/cli/command.js +29 -0
- package/dist/cli/command.js.map +1 -0
- package/dist/cli/commands/balance.d.ts +3 -0
- package/dist/cli/commands/balance.js +46 -0
- package/dist/cli/commands/balance.js.map +1 -0
- package/dist/cli/commands/bootstrap.d.ts +3 -0
- package/dist/cli/commands/bootstrap.js +165 -0
- package/dist/cli/commands/bootstrap.js.map +1 -0
- package/dist/cli/commands/claim-rewards.d.ts +3 -0
- package/dist/cli/commands/claim-rewards.js +121 -0
- package/dist/cli/commands/claim-rewards.js.map +1 -0
- package/dist/cli/commands/doctor.d.ts +3 -0
- package/dist/cli/commands/doctor.js +151 -0
- package/dist/cli/commands/doctor.js.map +1 -0
- package/dist/cli/commands/fleet-scale.d.ts +3 -0
- package/dist/cli/commands/fleet-scale.js +449 -0
- package/dist/cli/commands/fleet-scale.js.map +1 -0
- package/dist/cli/commands/fleet.d.ts +3 -0
- package/dist/cli/commands/fleet.js +45 -0
- package/dist/cli/commands/fleet.js.map +1 -0
- package/dist/cli/commands/fund-requirements.d.ts +3 -0
- package/dist/cli/commands/fund-requirements.js +139 -0
- package/dist/cli/commands/fund-requirements.js.map +1 -0
- package/dist/cli/commands/history.d.ts +3 -0
- package/dist/cli/commands/history.js +61 -0
- package/dist/cli/commands/history.js.map +1 -0
- package/dist/cli/commands/init.d.ts +3 -0
- package/dist/cli/commands/init.js +91 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/keys-backup.d.ts +3 -0
- package/dist/cli/commands/keys-backup.js +107 -0
- package/dist/cli/commands/keys-backup.js.map +1 -0
- package/dist/cli/commands/logs.d.ts +3 -0
- package/dist/cli/commands/logs.js +69 -0
- package/dist/cli/commands/logs.js.map +1 -0
- package/dist/cli/commands/rewards.d.ts +3 -0
- package/dist/cli/commands/rewards.js +54 -0
- package/dist/cli/commands/rewards.js.map +1 -0
- package/dist/cli/commands/run.d.ts +3 -0
- package/dist/cli/commands/run.js +96 -0
- package/dist/cli/commands/run.js.map +1 -0
- package/dist/cli/commands/status.d.ts +3 -0
- package/dist/cli/commands/status.js +54 -0
- package/dist/cli/commands/status.js.map +1 -0
- package/dist/cli/commands/stop.d.ts +3 -0
- package/dist/cli/commands/stop.js +82 -0
- package/dist/cli/commands/stop.js.map +1 -0
- package/dist/cli/commands/submit-intent.d.ts +3 -0
- package/dist/cli/commands/submit-intent.js +169 -0
- package/dist/cli/commands/submit-intent.js.map +1 -0
- package/dist/cli/commands/version.d.ts +3 -0
- package/dist/cli/commands/version.js +114 -0
- package/dist/cli/commands/version.js.map +1 -0
- package/dist/cli/commands/withdraw.d.ts +3 -0
- package/dist/cli/commands/withdraw.js +181 -0
- package/dist/cli/commands/withdraw.js.map +1 -0
- package/dist/cli/deployment-digest.d.ts +10 -0
- package/dist/cli/deployment-digest.js +25 -0
- package/dist/cli/deployment-digest.js.map +1 -0
- package/dist/cli/execution-context.d.ts +50 -0
- package/dist/cli/execution-context.js +154 -0
- package/dist/cli/execution-context.js.map +1 -0
- package/dist/cli/help.d.ts +3 -0
- package/dist/cli/help.js +37 -0
- package/dist/cli/help.js.map +1 -0
- package/dist/cli/index.d.ts +17 -0
- package/dist/cli/index.js +132 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/introspection-context.d.ts +10 -0
- package/dist/cli/introspection-context.js +60 -0
- package/dist/cli/introspection-context.js.map +1 -0
- package/dist/cli/output.d.ts +36 -0
- package/dist/cli/output.js +35 -0
- package/dist/cli/output.js.map +1 -0
- package/dist/cli/password.d.ts +12 -0
- package/dist/cli/password.js +51 -0
- package/dist/cli/password.js.map +1 -0
- package/dist/config.d.ts +174 -0
- package/dist/config.js +252 -0
- package/dist/config.js.map +1 -0
- package/dist/daemon/creator.d.ts +24 -0
- package/dist/daemon/creator.js +80 -0
- package/dist/daemon/creator.js.map +1 -0
- package/dist/daemon/daemon.d.ts +60 -0
- package/dist/daemon/daemon.js +158 -0
- package/dist/daemon/daemon.js.map +1 -0
- package/dist/daemon/delivery-watcher.d.ts +10 -0
- package/dist/daemon/delivery-watcher.js +37 -0
- package/dist/daemon/delivery-watcher.js.map +1 -0
- package/dist/daemon/restorer.d.ts +19 -0
- package/dist/daemon/restorer.js +82 -0
- package/dist/daemon/restorer.js.map +1 -0
- package/dist/daemon/reward-claim-loop.d.ts +38 -0
- package/dist/daemon/reward-claim-loop.js +48 -0
- package/dist/daemon/reward-claim-loop.js.map +1 -0
- package/dist/discovery/registry.d.ts +43 -0
- package/dist/discovery/registry.js +104 -0
- package/dist/discovery/registry.js.map +1 -0
- package/dist/discovery/subgraph.d.ts +37 -0
- package/dist/discovery/subgraph.js +87 -0
- package/dist/discovery/subgraph.js.map +1 -0
- package/dist/earning/bootstrap.d.ts +79 -0
- package/dist/earning/bootstrap.js +989 -0
- package/dist/earning/bootstrap.js.map +1 -0
- package/dist/earning/contracts.d.ts +431 -0
- package/dist/earning/contracts.js +518 -0
- package/dist/earning/contracts.js.map +1 -0
- package/dist/earning/evidence-simhash.d.ts +59 -0
- package/dist/earning/evidence-simhash.js +87 -0
- package/dist/earning/evidence-simhash.js.map +1 -0
- package/dist/earning/fleet-display-index.d.ts +8 -0
- package/dist/earning/fleet-display-index.js +12 -0
- package/dist/earning/fleet-display-index.js.map +1 -0
- package/dist/earning/fleet-retire.d.ts +28 -0
- package/dist/earning/fleet-retire.js +75 -0
- package/dist/earning/fleet-retire.js.map +1 -0
- package/dist/earning/jinn-rewards.d.ts +62 -0
- package/dist/earning/jinn-rewards.js +81 -0
- package/dist/earning/jinn-rewards.js.map +1 -0
- package/dist/earning/next-service-index.d.ts +4 -0
- package/dist/earning/next-service-index.js +7 -0
- package/dist/earning/next-service-index.js.map +1 -0
- package/dist/earning/orphan-sweep.d.ts +33 -0
- package/dist/earning/orphan-sweep.js +157 -0
- package/dist/earning/orphan-sweep.js.map +1 -0
- package/dist/earning/reconcile.d.ts +37 -0
- package/dist/earning/reconcile.js +216 -0
- package/dist/earning/reconcile.js.map +1 -0
- package/dist/earning/safe-adapter.d.ts +70 -0
- package/dist/earning/safe-adapter.js +228 -0
- package/dist/earning/safe-adapter.js.map +1 -0
- package/dist/earning/stolas-claim.d.ts +47 -0
- package/dist/earning/stolas-claim.js +115 -0
- package/dist/earning/stolas-claim.js.map +1 -0
- package/dist/earning/store.d.ts +36 -0
- package/dist/earning/store.js +156 -0
- package/dist/earning/store.js.map +1 -0
- package/dist/earning/types.d.ts +123 -0
- package/dist/earning/types.js +64 -0
- package/dist/earning/types.js.map +1 -0
- package/dist/earning/viem-clients.d.ts +9 -0
- package/dist/earning/viem-clients.js +22 -0
- package/dist/earning/viem-clients.js.map +1 -0
- package/dist/earning/wallet.d.ts +20 -0
- package/dist/earning/wallet.js +103 -0
- package/dist/earning/wallet.js.map +1 -0
- package/dist/errors/envelope.d.ts +41 -0
- package/dist/errors/envelope.js +48 -0
- package/dist/errors/envelope.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/main.d.ts +32 -0
- package/dist/main.js +281 -0
- package/dist/main.js.map +1 -0
- package/dist/mcp/server.d.ts +14 -0
- package/dist/mcp/server.js +205 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/operator-errors.d.ts +16 -0
- package/dist/operator-errors.js +88 -0
- package/dist/operator-errors.js.map +1 -0
- package/dist/preflight/claude-binary.d.ts +19 -0
- package/dist/preflight/claude-binary.js +44 -0
- package/dist/preflight/claude-binary.js.map +1 -0
- package/dist/preflight/claude-invocation-envelope.d.ts +11 -0
- package/dist/preflight/claude-invocation-envelope.js +67 -0
- package/dist/preflight/claude-invocation-envelope.js.map +1 -0
- package/dist/runner/claude.d.ts +15 -0
- package/dist/runner/claude.js +193 -0
- package/dist/runner/claude.js.map +1 -0
- package/dist/runner/runner.d.ts +11 -0
- package/dist/runner/runner.js +2 -0
- package/dist/runner/runner.js.map +1 -0
- package/dist/runner/simple.d.ts +8 -0
- package/dist/runner/simple.js +11 -0
- package/dist/runner/simple.js.map +1 -0
- package/dist/store/store.d.ts +74 -0
- package/dist/store/store.js +173 -0
- package/dist/store/store.js.map +1 -0
- package/dist/tx-retry.d.ts +55 -0
- package/dist/tx-retry.js +214 -0
- package/dist/tx-retry.js.map +1 -0
- package/dist/types/desired-state.d.ts +41 -0
- package/dist/types/desired-state.js +16 -0
- package/dist/types/desired-state.js.map +1 -0
- package/dist/types/errors.d.ts +8 -0
- package/dist/types/errors.js +17 -0
- package/dist/types/errors.js.map +1 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.js +3 -0
- package/dist/types/index.js.map +1 -0
- package/dist/withdraw/args.d.ts +29 -0
- package/dist/withdraw/args.js +198 -0
- package/dist/withdraw/args.js.map +1 -0
- package/dist/withdraw/run-withdraw-plan.d.ts +21 -0
- package/dist/withdraw/run-withdraw-plan.js +257 -0
- package/dist/withdraw/run-withdraw-plan.js.map +1 -0
- package/dist/x402/acquire.d.ts +6 -0
- package/dist/x402/acquire.js +32 -0
- package/dist/x402/acquire.js.map +1 -0
- package/dist/x402/facilitator.d.ts +11 -0
- package/dist/x402/facilitator.js +52 -0
- package/dist/x402/facilitator.js.map +1 -0
- package/dist/x402/handler.d.ts +15 -0
- package/dist/x402/handler.js +40 -0
- package/dist/x402/handler.js.map +1 -0
- 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
|