golem-vm-provider 0.1.50__py3-none-any.whl → 0.1.52__py3-none-any.whl

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: golem-vm-provider
3
- Version: 0.1.50
3
+ Version: 0.1.52
4
4
  Summary: VM on Golem Provider Node - Run your own provider node to offer VMs on the Golem Network
5
5
  Keywords: golem,vm,provider,cloud,decentralized
6
6
  Author: Phillip Jensen
@@ -436,6 +436,34 @@ poetry run golem-provider streams list --json
436
436
  poetry run golem-provider streams show <vm_id>
437
437
  ```
438
438
 
439
+ - Summarize earnings and withdrawable amounts:
440
+
441
+ ```bash
442
+ poetry run golem-provider streams earnings
443
+ # or JSON
444
+ poetry run golem-provider streams earnings --json
445
+ ```
446
+
447
+ - Withdraw vested funds:
448
+
449
+ ```bash
450
+ # One VM by id
451
+ poetry run golem-provider streams withdraw --vm-id <vm_id>
452
+
453
+ # All mapped streams
454
+ poetry run golem-provider streams withdraw --all
455
+ ```
456
+
457
+ Configure monitor and withdraw via CLI:
458
+
459
+ ```bash
460
+ # Set monitor to require 1h remaining, check every 30s
461
+ poetry run golem-provider config monitor --enable true --interval 30 --min-remaining 3600
462
+
463
+ # Enable auto-withdraw every 15 minutes when >= 1e15 wei
464
+ poetry run golem-provider config withdraw --enable true --interval 900 --min-wei 1000000000000000
465
+ ```
466
+
439
467
  ### Resource Advertisement Flow
440
468
 
441
469
  ```mermaid
@@ -13,7 +13,7 @@ provider/discovery/multi_advertiser.py,sha256=_J79wA1-XQ4GsLzt9KrKpWigGSGBqtut7D
13
13
  provider/discovery/resource_monitor.py,sha256=AmiEc7yBGEGXCunQ-QKmVgosDX3gOhK1Y58LJZXrwAs,949
14
14
  provider/discovery/resource_tracker.py,sha256=MP7IXd3aIMsjB4xz5Oj9zFDTEnvrnw-Cyxpl33xcJcc,6006
15
15
  provider/discovery/service.py,sha256=vX_mVSxvn3arnb2cKDM_SeJp1ZgPdImP2aUubeXgdRg,915
16
- provider/main.py,sha256=RSq2_dbBjQYkNwohxuwgzKbnzcqzEgJH1wcPMMf00t0,18925
16
+ provider/main.py,sha256=-dkQEwMrU2g1ljGrdf2B6P6CcgpDAjeCEqbBy_E4ybk,32799
17
17
  provider/network/port_verifier.py,sha256=3l6WNwBHydggJRFYkAsuBp1eCxaU619kjWuM-zSVj2o,13267
18
18
  provider/payments/blockchain_service.py,sha256=4GrzDKwCSUVoENqjD4RLyJ0qwBOJKMyVk5Li-XNsyTc,3567
19
19
  provider/payments/monitor.py,sha256=Rw17zYsxZre0zU6R0oeRNvVIzMdXLsgoUvSPHpJy6I0,4488
@@ -25,7 +25,7 @@ provider/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
25
25
  provider/utils/ascii_art.py,sha256=ykBFsztk57GIiz1NJ-EII5UvN74iECqQL4h9VmiW6Z8,3161
26
26
  provider/utils/logging.py,sha256=VV3oTYSRT8hUejtXLuua1M6kCHmIJgPspIkzsUVhYW0,1920
27
27
  provider/utils/port_display.py,sha256=u1HWQFA2kPbsM-TnsQfL6Hr4KmjIZWZfsjoxarHpbW0,11981
28
- provider/utils/pricing.py,sha256=e8obIH3yan8HsXJXoFvyh7eS1DtXJT5NZediPWhmJ0k,6113
28
+ provider/utils/pricing.py,sha256=uTgiBJ04LuVPKkzwMTVTb6Jb7m_Z3o5XLsoMh49ixqY,6315
29
29
  provider/utils/retry.py,sha256=GvBjpr0DpTOgw28M2hI0yt17dpYLRwrxUUqVxWHQPtM,3148
30
30
  provider/utils/setup.py,sha256=Z5dLuBQkb5vdoQsu1HJZwXmu9NWsiBYJ7Vq9-C-_tY8,2932
31
31
  provider/vm/__init__.py,sha256=LJL504QGbqZvBbMN3G9ixMgAwvOWAKW37zUm_EiaW9M,508
@@ -38,7 +38,7 @@ provider/vm/port_manager.py,sha256=iYSwjTjD_ziOhG8aI7juKHw1OwwRUTJQyQoRUNQvz9w,1
38
38
  provider/vm/provider.py,sha256=A7QN89EJjcSS40_SmKeinG1Jp_NGffJaLse-XdKciAs,1164
39
39
  provider/vm/proxy_manager.py,sha256=n4NTsyz2rtrvjtf_ceKBk-g2q_mzqPwruB1q7UlQVBc,14928
40
40
  provider/vm/service.py,sha256=Ki4SGNIZUq3XmaPMwAOoNzdZzKQsmFXid374wgjFPes,4636
41
- golem_vm_provider-0.1.50.dist-info/METADATA,sha256=h4dTmyA9UOyvi8R3pp13bW34sxr8W8JSoYzALN8gRmA,16585
42
- golem_vm_provider-0.1.50.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
43
- golem_vm_provider-0.1.50.dist-info/entry_points.txt,sha256=5Jiie1dIXygmxmDW66bKKxQpmBLJ7leSKRrb8bkQALw,52
44
- golem_vm_provider-0.1.50.dist-info/RECORD,,
41
+ golem_vm_provider-0.1.52.dist-info/METADATA,sha256=-9IAEK4SfcsssCSwvniqN0hBaIvw3V8yxpEi9plknLw,17288
42
+ golem_vm_provider-0.1.52.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
43
+ golem_vm_provider-0.1.52.dist-info/entry_points.txt,sha256=5Jiie1dIXygmxmDW66bKKxQpmBLJ7leSKRrb8bkQALw,52
44
+ golem_vm_provider-0.1.52.dist-info/RECORD,,
provider/main.py CHANGED
@@ -132,6 +132,8 @@ streams_app = typer.Typer(help="Inspect payment streams")
132
132
  cli.add_typer(pricing_app, name="pricing")
133
133
  cli.add_typer(wallet_app, name="wallet")
134
134
  cli.add_typer(streams_app, name="streams")
135
+ config_app = typer.Typer(help="Configure stream monitoring and withdrawals")
136
+ cli.add_typer(config_app, name="config")
135
137
 
136
138
  @cli.callback()
137
139
  def main():
@@ -189,6 +191,9 @@ def streams_list(json_out: bool = typer.Option(False, "--json", help="Output in
189
191
  from .container import Container
190
192
  from .config import settings
191
193
  from .payments.blockchain_service import StreamPaymentReader
194
+ from .utils.pricing import fetch_glm_usd_price, fetch_eth_usd_price
195
+ from decimal import Decimal
196
+ from web3 import Web3
192
197
  import json as _json
193
198
  try:
194
199
  if not settings.STREAM_PAYMENT_ADDRESS or settings.STREAM_PAYMENT_ADDRESS == "0x0000000000000000000000000000000000000000":
@@ -211,6 +216,7 @@ def streams_list(json_out: bool = typer.Option(False, "--json", help="Output in
211
216
  rows.append({
212
217
  "vm_id": vm_id,
213
218
  "stream_id": int(stream_id),
219
+ "token": str(s.get("token")),
214
220
  "recipient": s["recipient"],
215
221
  "start": int(s["startTime"]),
216
222
  "stop": int(s["stopTime"]),
@@ -230,12 +236,66 @@ def streams_list(json_out: bool = typer.Option(False, "--json", help="Output in
230
236
  if not rows:
231
237
  print("No streams mapped.")
232
238
  return
233
- print("\nStreams (VM stream_id, remaining s, verified):")
239
+ # Prepare human-friendly display (ETH/GLM + USD)
240
+ ZERO = "0x0000000000000000000000000000000000000000"
241
+ # Cache prices so we don't query per-row
242
+ price_cache: dict[str, Optional[Decimal]] = {"ETH": None, "GLM": None}
243
+ # Determine which symbols are present
244
+ symbols_present = set()
234
245
  for r in rows:
235
246
  if "error" in r:
236
- print(f"- {r['vm_id']}: {r['stream_id']} ERROR: {r['error']}")
237
- else:
238
- print(f"- {r['vm_id']}: {r['stream_id']} remaining={r['remaining']}s verified={r['verified']} reason={r['reason']} withdrawable={r['withdrawable']}")
247
+ continue
248
+ token_addr = (r.get("token") or "").lower()
249
+ sym = "ETH" if token_addr == ZERO.lower() else "GLM"
250
+ symbols_present.add(sym)
251
+ if "ETH" in symbols_present:
252
+ price_cache["ETH"] = fetch_eth_usd_price()
253
+ if "GLM" in symbols_present:
254
+ price_cache["GLM"] = fetch_glm_usd_price()
255
+
256
+ # Build table rows
257
+ table_rows = []
258
+ for r in rows:
259
+ if "error" in r:
260
+ table_rows.append([r["vm_id"], str(r["stream_id"]), "—", "ERROR", r.get("error", ""), "—"])
261
+ continue
262
+ token_addr = (r.get("token") or "").lower()
263
+ sym = "ETH" if token_addr == ZERO.lower() else "GLM"
264
+ withdrawable_eth = Decimal(str(Web3.from_wei(int(r["withdrawable"]), "ether")))
265
+ withdrawable_str = f"{withdrawable_eth:.6f} {sym}"
266
+ price = price_cache.get(sym)
267
+ usd_str = "—"
268
+ if price is not None:
269
+ try:
270
+ usd_val = (withdrawable_eth * price).quantize(Decimal("0.01"))
271
+ usd_str = f"${usd_val}"
272
+ except Exception:
273
+ usd_str = "—"
274
+ table_rows.append([
275
+ r["vm_id"],
276
+ str(r["stream_id"]),
277
+ f"{int(r['remaining'])}s",
278
+ "yes" if r["verified"] else "no",
279
+ withdrawable_str,
280
+ usd_str,
281
+ ])
282
+
283
+ headers = ["VM", "Stream", "Remaining", "Verified", "Withdrawable", "USD"]
284
+ # Compute column widths
285
+ cols = len(headers)
286
+ col_widths = [len(h) for h in headers]
287
+ for row in table_rows:
288
+ for i in range(cols):
289
+ col_widths[i] = max(col_widths[i], len(str(row[i])))
290
+
291
+ def fmt_row(values: list[str]) -> str:
292
+ return " ".join(str(values[i]).ljust(col_widths[i]) for i in range(cols))
293
+
294
+ print("\nStreams")
295
+ print(fmt_row(headers))
296
+ print(" ".join("-" * w for w in col_widths))
297
+ for row in table_rows:
298
+ print(fmt_row(row))
239
299
  except Exception as e:
240
300
  print(f"Error: {e}")
241
301
  raise typer.Exit(code=1)
@@ -247,6 +307,9 @@ def streams_show(vm_id: str = typer.Argument(..., help="VM id (requestor_name)")
247
307
  from .container import Container
248
308
  from .config import settings
249
309
  from .payments.blockchain_service import StreamPaymentReader
310
+ from .utils.pricing import fetch_glm_usd_price, fetch_eth_usd_price
311
+ from decimal import Decimal
312
+ from web3 import Web3
250
313
  import json as _json
251
314
  try:
252
315
  c = Container()
@@ -279,7 +342,189 @@ def streams_show(vm_id: str = typer.Argument(..., help="VM id (requestor_name)")
279
342
  if json_out:
280
343
  print(_json.dumps(out, indent=2))
281
344
  else:
282
- print(f"VM {vm_id}: stream {sid} remaining={remaining}s verified={ok} withdrawable={withdrawable}")
345
+ ZERO = "0x0000000000000000000000000000000000000000"
346
+ token_addr = (s.get("token") or "").lower()
347
+ sym = "ETH" if token_addr == ZERO.lower() else "GLM"
348
+ nat = Decimal(str(Web3.from_wei(int(withdrawable), "ether")))
349
+ price = fetch_eth_usd_price() if sym == "ETH" else fetch_glm_usd_price()
350
+ usd_str = "—"
351
+ if price is not None:
352
+ try:
353
+ usd_val = (nat * price).quantize(Decimal("0.01"))
354
+ usd_str = f"${usd_val}"
355
+ except Exception:
356
+ usd_str = "—"
357
+ def _fmt_seconds(sec: int) -> str:
358
+ m, s2 = divmod(int(sec), 60)
359
+ h, m = divmod(m, 60)
360
+ d, h = divmod(h, 24)
361
+ parts = []
362
+ if d: parts.append(f"{d}d")
363
+ if h: parts.append(f"{h}h")
364
+ if m and not d: parts.append(f"{m}m")
365
+ if s2 and not d and not h and not m: parts.append(f"{s2}s")
366
+ return " ".join(parts) or "0s"
367
+ # Pretty single-record display
368
+ print("\nStream Details")
369
+ headers = ["VM", "Stream", "Remaining", "Verified", "Withdrawable", "USD"]
370
+ cols = [
371
+ vm_id,
372
+ str(sid),
373
+ _fmt_seconds(remaining),
374
+ "yes" if ok else "no",
375
+ f"{nat:.6f} {sym}",
376
+ usd_str,
377
+ ]
378
+ w = [max(len(headers[i]), len(str(cols[i]))) for i in range(len(headers))]
379
+ print(" ".join(headers[i].ljust(w[i]) for i in range(len(w))))
380
+ print(" ".join("-" * wi for wi in w))
381
+ print(" ".join(str(cols[i]).ljust(w[i]) for i in range(len(w))))
382
+ except Exception as e:
383
+ print(f"Error: {e}")
384
+ raise typer.Exit(code=1)
385
+
386
+ @streams_app.command("earnings")
387
+ def streams_earnings(json_out: bool = typer.Option(False, "--json", help="Output in JSON")):
388
+ """Summarize provider earnings: vested, withdrawn, and withdrawable totals."""
389
+ from .container import Container
390
+ from .config import settings
391
+ from .payments.blockchain_service import StreamPaymentReader
392
+ from .utils.pricing import fetch_glm_usd_price, fetch_eth_usd_price
393
+ from decimal import Decimal
394
+ from web3 import Web3
395
+ import json as _json
396
+ try:
397
+ c = Container()
398
+ c.config.from_pydantic(settings)
399
+ stream_map = c.stream_map()
400
+ reader = StreamPaymentReader(settings.POLYGON_RPC_URL, settings.STREAM_PAYMENT_ADDRESS)
401
+ items = asyncio.run(stream_map.all_items())
402
+ now = int(reader.web3.eth.get_block("latest")["timestamp"]) if items else 0
403
+ rows = []
404
+ total_vested = 0
405
+ total_withdrawn = 0
406
+ total_withdrawable = 0
407
+ ZERO = "0x0000000000000000000000000000000000000000"
408
+ sums_native: dict[str, Decimal] = {"ETH": Decimal("0"), "GLM": Decimal("0")}
409
+ for vm_id, stream_id in items.items():
410
+ try:
411
+ s = reader.get_stream(int(stream_id))
412
+ vested = max(min(now, int(s["stopTime"])) - int(s["startTime"]), 0) * int(s["ratePerSecond"]) # type: ignore
413
+ withdrawable = max(int(vested) - int(s["withdrawn"]), 0)
414
+ total_vested += int(vested)
415
+ total_withdrawn += int(s["withdrawn"]) # type: ignore
416
+ total_withdrawable += int(withdrawable)
417
+ sym = "ETH" if (s.get("token") or "").lower() == ZERO.lower() else "GLM"
418
+ sums_native[sym] += Decimal(str(Web3.from_wei(int(withdrawable), "ether")))
419
+ rows.append({
420
+ "vm_id": vm_id,
421
+ "stream_id": int(stream_id),
422
+ "token": str(s.get("token")),
423
+ "vested": int(vested),
424
+ "withdrawn": int(s["withdrawn"]),
425
+ "withdrawable": int(withdrawable),
426
+ })
427
+ except Exception as e:
428
+ rows.append({"vm_id": vm_id, "stream_id": int(stream_id), "error": str(e)})
429
+ out = {
430
+ "streams": rows,
431
+ "totals": {
432
+ "vested": int(total_vested),
433
+ "withdrawn": int(total_withdrawn),
434
+ "withdrawable": int(total_withdrawable),
435
+ }
436
+ }
437
+ if json_out:
438
+ print(_json.dumps(out, indent=2))
439
+ return
440
+ # Human summary by token with USD
441
+ price_eth = fetch_eth_usd_price()
442
+ price_glm = fetch_glm_usd_price()
443
+ def _fmt_usd(amount_native: Decimal, price: Optional[Decimal]) -> str:
444
+ if price is None:
445
+ return "—"
446
+ try:
447
+ return f"${(amount_native * price).quantize(Decimal('0.01'))}"
448
+ except Exception:
449
+ return "—"
450
+ print("\nEarnings Summary")
451
+ headers = ["Token", "Withdrawable", "USD"]
452
+ data_rows = [
453
+ ["ETH", f"{sums_native['ETH']:.6f} ETH", _fmt_usd(sums_native["ETH"], price_eth)],
454
+ ["GLM", f"{sums_native['GLM']:.6f} GLM", _fmt_usd(sums_native["GLM"], price_glm)],
455
+ ]
456
+ # Table widths
457
+ w = [len(h) for h in headers]
458
+ for r in data_rows:
459
+ for i in range(3):
460
+ w[i] = max(w[i], len(str(r[i])))
461
+ print(" ".join(headers[i].ljust(w[i]) for i in range(3)))
462
+ print(" ".join("-" * wi for wi in w))
463
+ for r in data_rows:
464
+ print(" ".join(str(r[i]).ljust(w[i]) for i in range(3)))
465
+ # Per stream table
466
+ if rows:
467
+ table = []
468
+ for r in rows:
469
+ if "error" in r:
470
+ table.append([r["vm_id"], str(r["stream_id"]), "ERROR", r.get("error", "")])
471
+ continue
472
+ sym = "ETH" if (r.get("token") or "").lower() == ZERO.lower() else "GLM"
473
+ nat = Decimal(str(Web3.from_wei(int(r["withdrawable"]), "ether")))
474
+ price = price_eth if sym == "ETH" else price_glm
475
+ usd = _fmt_usd(nat, price)
476
+ table.append([r["vm_id"], str(r["stream_id"]), f"{nat:.6f} {sym}", usd])
477
+ h2 = ["VM", "Stream", "Withdrawable", "USD"]
478
+ w2 = [len(x) for x in h2]
479
+ for row in table:
480
+ for i in range(4):
481
+ w2[i] = max(w2[i], len(str(row[i])))
482
+ print("\nPer Stream")
483
+ print(" ".join(h2[i].ljust(w2[i]) for i in range(4)))
484
+ print(" ".join("-" * wi for wi in w2))
485
+ for row in table:
486
+ print(" ".join(str(row[i]).ljust(w2[i]) for i in range(4)))
487
+ except Exception as e:
488
+ print(f"Error: {e}")
489
+ raise typer.Exit(code=1)
490
+
491
+
492
+ @streams_app.command("withdraw")
493
+ def streams_withdraw(
494
+ vm_id: str = typer.Option(None, "--vm-id", help="Withdraw for a single VM id"),
495
+ all_streams: bool = typer.Option(False, "--all", help="Withdraw for all mapped streams"),
496
+ ):
497
+ """Withdraw vested funds for one or all streams."""
498
+ from .container import Container
499
+ from .config import settings
500
+ try:
501
+ if not vm_id and not all_streams:
502
+ print("Specify --vm-id or --all")
503
+ raise typer.Exit(code=1)
504
+ c = Container()
505
+ c.config.from_pydantic(settings)
506
+ stream_map = c.stream_map()
507
+ client = c.stream_client()
508
+ targets = []
509
+ if all_streams:
510
+ items = asyncio.run(stream_map.all_items())
511
+ for vid, sid in items.items():
512
+ targets.append((vid, int(sid)))
513
+ else:
514
+ sid = asyncio.run(stream_map.get(vm_id))
515
+ if sid is None:
516
+ print("No stream mapped for this VM.")
517
+ raise typer.Exit(code=1)
518
+ targets.append((vm_id, int(sid)))
519
+ results = []
520
+ for vid, sid in targets:
521
+ try:
522
+ tx = client.withdraw(int(sid))
523
+ results.append((vid, sid, tx))
524
+ print(f"Withdrew stream {sid} for VM {vid}: tx={tx}")
525
+ except Exception as e:
526
+ print(f"Failed to withdraw stream {sid} for VM {vid}: {e}")
527
+ # no JSON aggregation here; use earnings for structured output
283
528
  except Exception as e:
284
529
  print(f"Error: {e}")
285
530
  raise typer.Exit(code=1)
@@ -331,6 +576,80 @@ def _write_env_vars(path: str, updates: dict):
331
576
  with open(path, "w") as f:
332
577
  f.writelines(out)
333
578
 
579
+
580
+ @config_app.command("withdraw")
581
+ def config_withdraw(
582
+ enable: bool = typer.Option(None, "--enable", help="Enable/disable auto-withdraw (true/false)"),
583
+ interval: int = typer.Option(None, "--interval", help="Withdraw interval in seconds (e.g., 1800)"),
584
+ min_wei: int = typer.Option(None, "--min-wei", help="Only withdraw when >= this wei amount"),
585
+ dev: bool = typer.Option(False, "--dev", help="Write to .env.dev instead of .env"),
586
+ ):
587
+ """Configure provider auto-withdraw settings and persist to .env(.dev)."""
588
+ from .config import settings
589
+ env_path = _env_path_for(dev)
590
+ updates = {}
591
+ if enable is not None:
592
+ updates["GOLEM_PROVIDER_STREAM_WITHDRAW_ENABLED"] = str(enable).lower()
593
+ settings.STREAM_WITHDRAW_ENABLED = bool(enable)
594
+ if interval is not None:
595
+ if interval < 0:
596
+ raise typer.BadParameter("--interval must be >= 0")
597
+ updates["GOLEM_PROVIDER_STREAM_WITHDRAW_INTERVAL_SECONDS"] = int(interval)
598
+ try:
599
+ settings.STREAM_WITHDRAW_INTERVAL_SECONDS = int(interval)
600
+ except Exception:
601
+ pass
602
+ if min_wei is not None:
603
+ if min_wei < 0:
604
+ raise typer.BadParameter("--min-wei must be >= 0")
605
+ updates["GOLEM_PROVIDER_STREAM_MIN_WITHDRAW_WEI"] = int(min_wei)
606
+ try:
607
+ settings.STREAM_MIN_WITHDRAW_WEI = int(min_wei)
608
+ except Exception:
609
+ pass
610
+ if not updates:
611
+ print("No changes (use --enable/--interval/--min-wei)")
612
+ raise typer.Exit(code=0)
613
+ _write_env_vars(env_path, updates)
614
+ print(f"Updated withdraw settings in {env_path}")
615
+
616
+
617
+ @config_app.command("monitor")
618
+ def config_monitor(
619
+ enable: bool = typer.Option(None, "--enable", help="Enable/disable stream monitor (true/false)"),
620
+ interval: int = typer.Option(None, "--interval", help="Monitor interval in seconds (e.g., 30)"),
621
+ min_remaining: int = typer.Option(None, "--min-remaining", help="Minimum remaining runway to keep VM running (seconds)"),
622
+ dev: bool = typer.Option(False, "--dev", help="Write to .env.dev instead of .env"),
623
+ ):
624
+ """Configure provider stream monitor and persist to .env(.dev)."""
625
+ from .config import settings
626
+ env_path = _env_path_for(dev)
627
+ updates = {}
628
+ if enable is not None:
629
+ updates["GOLEM_PROVIDER_STREAM_MONITOR_ENABLED"] = str(enable).lower()
630
+ settings.STREAM_MONITOR_ENABLED = bool(enable)
631
+ if interval is not None:
632
+ if interval < 0:
633
+ raise typer.BadParameter("--interval must be >= 0")
634
+ updates["GOLEM_PROVIDER_STREAM_MONITOR_INTERVAL_SECONDS"] = int(interval)
635
+ try:
636
+ settings.STREAM_MONITOR_INTERVAL_SECONDS = int(interval)
637
+ except Exception:
638
+ pass
639
+ if min_remaining is not None:
640
+ if min_remaining < 0:
641
+ raise typer.BadParameter("--min-remaining must be >= 0")
642
+ updates["GOLEM_PROVIDER_STREAM_MIN_REMAINING_SECONDS"] = int(min_remaining)
643
+ try:
644
+ settings.STREAM_MIN_REMAINING_SECONDS = int(min_remaining)
645
+ except Exception:
646
+ pass
647
+ if not updates:
648
+ print("No changes (use --enable/--interval/--min-remaining)")
649
+ raise typer.Exit(code=0)
650
+ _write_env_vars(env_path, updates)
651
+ print(f"Updated monitor settings in {env_path}")
652
+
334
653
  def _print_pricing_examples(glm_usd):
335
654
  from decimal import Decimal
336
655
  from .utils.pricing import calculate_monthly_cost, calculate_monthly_cost_usd
provider/utils/pricing.py CHANGED
@@ -47,6 +47,14 @@ def fetch_glm_usd_price() -> Optional[Decimal]:
47
47
  return _coingecko_simple_price(settings.COINGECKO_IDS)
48
48
 
49
49
 
50
+ def fetch_eth_usd_price() -> Optional[Decimal]:
51
+ """Fetch the current ETH price in USD from CoinGecko.
52
+
53
+ Uses the canonical "ethereum" id.
54
+ """
55
+ return _coingecko_simple_price("ethereum")
56
+
57
+
50
58
  def usd_to_glm(usd_amount: Decimal, glm_usd: Decimal) -> Decimal:
51
59
  if glm_usd <= 0:
52
60
  raise ValueError("Invalid GLM/USD price")