* Attempt to stake credits so this node becomes eligible for validation tasks. * Safe to call repeatedly; internally throttled by the retry state machine. * * @param {{ amount?: number, force?: boolean }} [opts]
(opts)
| 204 | * @param {{ amount?: number, force?: boolean }} [opts] |
| 205 | */ |
| 206 | async function ensureValidatorStake(opts) { |
| 207 | const options = opts || {}; |
| 208 | const now = Date.now(); |
| 209 | |
| 210 | _loadStateFromDisk(); |
| 211 | |
| 212 | if (_state.disabledUntilRestart && !options.force) { |
| 213 | return { ok: false, skipped: 'disabled_until_restart' }; |
| 214 | } |
| 215 | |
| 216 | if (!options.force && _state.nextAttemptAt > now) { |
| 217 | return { ok: true, skipped: 'backoff', nextAttemptAt: _state.nextAttemptAt }; |
| 218 | } |
| 219 | |
| 220 | const nodeId = getNodeId(); |
| 221 | if (!nodeId) { |
| 222 | _state.nextAttemptAt = now + pickDelay('transient'); |
| 223 | _persistState(); |
| 224 | return { ok: false, error: 'no_node_id' }; |
| 225 | } |
| 226 | |
| 227 | const hubUrl = resolveHubUrl(); |
| 228 | const url = hubUrl.replace(/\/+$/, '') + '/a2a/validator/stake'; |
| 229 | const amount = Math.max(100, Math.round(Number(options.amount) || DEFAULT_STAKE_AMOUNT)); |
| 230 | |
| 231 | const controller = new AbortController(); |
| 232 | const timer = setTimeout(() => controller.abort(), STAKE_TIMEOUT_MS); |
| 233 | |
| 234 | const body = { |
| 235 | sender_id: nodeId, |
| 236 | node_id: nodeId, |
| 237 | payload: { stake_amount: amount }, |
| 238 | message_id: 'msg_' + Date.now().toString(36) + '_' + crypto.randomBytes(3).toString('hex'), |
| 239 | timestamp: new Date().toISOString(), |
| 240 | }; |
| 241 | |
| 242 | logStakeEvent('attempt', { node_id: nodeId, amount, hub: hubUrl }); |
| 243 | |
| 244 | let res; |
| 245 | let text = ''; |
| 246 | try { |
| 247 | res = await hubFetch(url, { |
| 248 | method: 'POST', |
| 249 | headers: buildStakeHubHeaders(), |
| 250 | body: JSON.stringify(body), |
| 251 | signal: controller.signal, |
| 252 | }); |
| 253 | clearTimeout(timer); |
| 254 | text = await res.text(); |
| 255 | } catch (err) { |
| 256 | clearTimeout(timer); |
| 257 | _state.transientFailures += 1; |
| 258 | _state.nextAttemptAt = now + pickDelay('transient'); |
| 259 | _persistState(); |
| 260 | const msg = err && err.message ? err.message : String(err); |
| 261 | logStakeEvent('failed_network', { |
| 262 | node_id: nodeId, |
| 263 | error: msg, |
no test coverage detected