request-vm-on-golem 0.1.45__py3-none-any.whl → 0.1.47__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: request-vm-on-golem
3
- Version: 0.1.45
3
+ Version: 0.1.47
4
4
  Summary: VM on Golem Requestor CLI - Create and manage virtual machines on the Golem Network
5
5
  Keywords: golem,vm,cloud,decentralized,cli
6
6
  Author: Phillip Jensen
@@ -158,6 +158,19 @@ poetry run golem vm stream topup --stream-id 123 --hours 3
158
158
  poetry run golem vm stream topup --stream-id 123 --glm 25.0
159
159
  ```
160
160
 
161
+ - Check stream status via provider (by VM name recorded in your DB):
162
+
163
+ ```bash
164
+ poetry run golem vm stream status my-vm
165
+ # add --json for machine-readable output
166
+ ```
167
+
168
+ - Inspect a stream directly on-chain:
169
+
170
+ ```bash
171
+ poetry run golem vm stream inspect --stream-id 123
172
+ ```
173
+
161
174
  - Create a VM and attach an existing stream (no auto-streams are created by the requestor):
162
175
 
163
176
  ```bash
@@ -170,8 +183,9 @@ poetry run golem vm create my-vm \
170
183
  Environment (env prefix `GOLEM_REQUESTOR_`):
171
184
 
172
185
  - `polygon_rpc_url` — EVM RPC URL (default L2 RPC)
173
- - `stream_payment_address` — StreamPayment address (fallback if provider doesn’t advertise)
174
- - `glm_token_address` — Token address; set to zero address to use native ETH
186
+ - `stream_payment_address` — StreamPayment address (defaults from `contracts/deployments/l2.json`; overridden by provider info)
187
+ - `glm_token_address` — Token address (defaults from `contracts/deployments/l2.json`; zero address means native ETH)
188
+ - Optional override of deployments directory: set `GOLEM_DEPLOYMENTS_DIR` to a folder containing `l2.json`.
175
189
  - `provider_eth_address` — optional dev helper; in production always use `/provider/info`
176
190
  - `network` — Target network for discovery filtering: `testnet` (default) or `mainnet`
177
191
 
@@ -1,14 +1,15 @@
1
1
  requestor/__init__.py,sha256=OqSUAh1uZBMx7GW0MoSMg967PVdmT8XdPJx3QYjwkak,116
2
2
  requestor/api/main.py,sha256=7utCzFNbh5Ol-vsBWeSwT4lXeHD7zdA-GFZuS3rHMWc,2180
3
3
  requestor/cli/__init__.py,sha256=e3E4oEGxmGj-STPtFkQwg_qIWhR0JAiAQdw3G1hXciU,37
4
- requestor/cli/commands.py,sha256=YiJvCWDp__c8OZ1YdrVc7gAYRUab5ygNGPCGxDmn3jE,35288
5
- requestor/config.py,sha256=v6557eetioJmuOPWKV4iAl7DeqGjgQ0gegqc0zEzJZU,6396
4
+ requestor/cli/commands.py,sha256=k2UNXNLlxFUq36IeU_VQVxrhp5sQkHW1EW-f8RtzXPY,42591
5
+ requestor/config.py,sha256=2ayNJzvIIoU0jMAVqbs-yfG4H63W_uALLScBG4EjUOw,8241
6
+ requestor/data/deployments/l2.json,sha256=XTNN2C5LkBfp4YbDKdUKfWMdp1fKnfv8D3TgcwVWxtQ,249
6
7
  requestor/db/__init__.py,sha256=Gm5DfWls6uvCZZ3HGGnyRHswbUQdeA5OGN8yPwH0hc8,88
7
8
  requestor/db/sqlite.py,sha256=l5pWbx2qlHuar1N_a0B9tVnmumLJY1w5rp3yZ7jmsC0,4146
8
9
  requestor/errors.py,sha256=wVpHBuYgQx5pTe_SamugfK-k768noikY1RxvPOjQGko,665
9
10
  requestor/payments/blockchain_service.py,sha256=EejW51A6Xqc3PKnKnzjRQ6pIVkH1FqacG4lwQAQ0HiM,6888
10
11
  requestor/provider/__init__.py,sha256=fmW23aYUVciF8-gmBZkG-PLhn22upmcDzdPfAOLHG6g,103
11
- requestor/provider/client.py,sha256=WXCm-1bytcgsuHEZzpg7RjjDOTuaXC9cj0Mrm7e6DSw,3676
12
+ requestor/provider/client.py,sha256=pfJymufYR13W4kfykHZSVvs6ikRUE5AdHp0W0DB17AE,4130
12
13
  requestor/run.py,sha256=GqOG6n34szt8Sp3SEqjRV6huWm737yCN6YnBqoiwI-U,1785
13
14
  requestor/security/faucet.py,sha256=35d8mD3fM0YqRIhUXuIKandOL8vbw2T2IFQWVsan9Lw,2056
14
15
  requestor/services/__init__.py,sha256=1qSn_6RMn0KB0A7LCnY2IW6_tC3HBQsdfkFeV-h94eM,172
@@ -20,7 +21,7 @@ requestor/ssh/__init__.py,sha256=hNgSqJ5s1_AwwxVRyFjUqh_LTBpI4Hmzq0F-f_wXN9g,119
20
21
  requestor/ssh/manager.py,sha256=3jQtbbK7CVC2yD1zCO88jGXh2fBcuv3CzWEqDLuaQVk,9758
21
22
  requestor/utils/logging.py,sha256=oFNpO8pJboYM8Wp7g3HOU4HFyBTKypVdY15lUiz1a4I,3721
22
23
  requestor/utils/spinner.py,sha256=PUHJdTD9jpUHur__01_qxXy87WFfNmjQbD_sLG-KlGo,2459
23
- request_vm_on_golem-0.1.45.dist-info/METADATA,sha256=Ln_GhY2wyZ0feb3EgI0x12zbnc1Zm0u6lyaNFRTF8LI,13912
24
- request_vm_on_golem-0.1.45.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
25
- request_vm_on_golem-0.1.45.dist-info/entry_points.txt,sha256=Z-skRNpJ8aZcIl_En9mEm1ygkp9FKy0bzQoL3zO52-0,44
26
- request_vm_on_golem-0.1.45.dist-info/RECORD,,
24
+ request_vm_on_golem-0.1.47.dist-info/METADATA,sha256=ABsicVTrzAVerDTSojxNI1UiqOw1eySap4DqjLlH1GU,14363
25
+ request_vm_on_golem-0.1.47.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
26
+ request_vm_on_golem-0.1.47.dist-info/entry_points.txt,sha256=Z-skRNpJ8aZcIl_En9mEm1ygkp9FKy0bzQoL3zO52-0,44
27
+ request_vm_on_golem-0.1.47.dist-info/RECORD,,
requestor/cli/commands.py CHANGED
@@ -186,10 +186,11 @@ async def list_providers(cpu: Optional[int], memory: Optional[int], storage: Opt
186
186
  @click.option('--memory', type=int, required=True, help='Memory in GB')
187
187
  @click.option('--storage', type=int, required=True, help='Disk in GB')
188
188
  @click.option('--stream-id', type=int, default=None, help='Optional StreamPayment stream id to fund this VM')
189
+ @click.option('--hours', type=int, default=1, help='If no stream-id is provided and payments are enabled, open a stream with this many hours of deposit (default 1)')
189
190
  @click.option('--yes', is_flag=True, help='Do not prompt for confirmation')
190
191
  @click.option('--network', type=click.Choice(['testnet', 'mainnet']), default=None, help='Override network for discovery during creation')
191
192
  @async_command
192
- async def create_vm(name: str, provider_id: str, cpu: int, memory: int, storage: int, stream_id: int | None, yes: bool, network: Optional[str] = None):
193
+ async def create_vm(name: str, provider_id: str, cpu: int, memory: int, storage: int, stream_id: int | None, hours: int, yes: bool, network: Optional[str] = None):
193
194
  """Create a new VM on a specific provider."""
194
195
  try:
195
196
  if network:
@@ -227,10 +228,7 @@ async def create_vm(name: str, provider_id: str, cpu: int, memory: int, storage:
227
228
  if est_glm != '—':
228
229
  price_str += f" (~{est_glm} GLM/mo)"
229
230
  click.echo(click.style(f" 💵 Estimated Monthly Cost: {price_str}", fg='yellow', bold=True))
230
- if not yes:
231
- if not click.confirm("Proceed with VM creation?", default=True):
232
- logger.warning("Creation cancelled by user")
233
- return
231
+ # For streamlined UX, proceed without an interactive confirmation
234
232
 
235
233
  # Setup SSH
236
234
  ssh_service = SSHService(config.ssh_key_dir)
@@ -239,8 +237,46 @@ async def create_vm(name: str, provider_id: str, cpu: int, memory: int, storage:
239
237
  # Initialize VM service
240
238
  provider_url = config.get_provider_url(provider_ip)
241
239
  async with ProviderClient(provider_url) as client:
240
+ # Fetch provider info if available (for preferred contract addresses); proceed regardless
241
+ info = None
242
+ try:
243
+ info = await client.get_provider_info()
244
+ except Exception:
245
+ info = None
246
+ # Always auto-open a stream when none provided (assume streaming required by default)
247
+ if stream_id is None:
248
+ # Compute rate from provider pricing
249
+ est = provider_service.compute_estimate(provider, (cpu, memory, storage))
250
+ if not est or est.get('glm_per_month') is None:
251
+ raise RequestorError('Provider requires streaming but does not advertise GLM pricing; cannot compute ratePerSecond')
252
+ glm_month = est['glm_per_month']
253
+ glm_per_second = float(glm_month) / (730.0 * 3600.0)
254
+ rate_per_second_wei = int(glm_per_second * (10**18))
255
+ deposit_wei = rate_per_second_wei * int(hours) * 3600
256
+ # Auto-fund via faucet if needed (testnets), then create stream
257
+ try:
258
+ from eth_account import Account
259
+ from ..security.faucet import L2FaucetService
260
+ acct = Account.from_key(config.ethereum_private_key)
261
+ faucet = L2FaucetService(config)
262
+ await faucet.request_funds(acct.address)
263
+ except Exception:
264
+ # Non-fatal; stream creation may still succeed if already funded
265
+ pass
266
+ # Open stream on-chain
267
+ from ..payments.blockchain_service import StreamPaymentClient, StreamPaymentConfig
268
+ spc = StreamPaymentConfig(
269
+ rpc_url=config.polygon_rpc_url,
270
+ contract_address=(info.get('stream_payment_address') if info else None) or config.stream_payment_address,
271
+ glm_token_address=(info.get('glm_token_address') if info else None) or config.glm_token_address,
272
+ private_key=config.ethereum_private_key,
273
+ )
274
+ sp_client = StreamPaymentClient(spc)
275
+ recipient = (info.get('provider_id') if info else None) or provider_id
276
+ stream_id = sp_client.create_stream(recipient, int(deposit_wei), int(rate_per_second_wei))
277
+ logger.success(f"Opened stream id={stream_id} (hours={hours})")
278
+
242
279
  vm_service = VMService(db_service, ssh_service, client)
243
-
244
280
  # Create VM
245
281
  vm = await vm_service.create_vm(
246
282
  name=name,
@@ -384,6 +420,97 @@ async def stream_topup(stream_id: int, glm: float | None, hours: int | None):
384
420
  raise click.Abort()
385
421
 
386
422
 
423
+ @vm_stream.command('status')
424
+ @click.argument('name')
425
+ @click.option('--json', 'as_json', is_flag=True, help='Output in JSON format')
426
+ @async_command
427
+ async def stream_status(name: str, as_json: bool):
428
+ """Show the payment stream status for a VM by name."""
429
+ try:
430
+ # Resolve VM and provider
431
+ vm = await db_service.get_vm(name)
432
+ if not vm:
433
+ raise RequestorError(f"VM '{name}' not found in local DB")
434
+ provider_url = config.get_provider_url(vm['provider_ip'])
435
+ async with ProviderClient(provider_url) as client:
436
+ status = await client.get_vm_stream_status(vm['vm_id'])
437
+ if as_json:
438
+ click.echo(json.dumps(status, indent=2))
439
+ return
440
+ # Pretty print
441
+ c = status.get('chain', {})
442
+ comp = status.get('computed', {})
443
+ click.echo("\n" + "─" * 60)
444
+ click.echo(click.style(f" 💸 Stream Status for VM: {name}", fg="blue", bold=True))
445
+ click.echo("─" * 60)
446
+ click.echo(f" Stream ID : {click.style(str(status.get('stream_id')), fg='cyan')}")
447
+ click.echo(f" Verified : {click.style(str(status.get('verified')), fg='green' if status.get('verified') else 'yellow')}")
448
+ click.echo(f" Reason : {status.get('reason')}")
449
+ click.echo(" On-chain :")
450
+ click.echo(f" recipient : {c.get('recipient')} ")
451
+ click.echo(f" startTime : {c.get('startTime')} stopTime: {c.get('stopTime')}")
452
+ click.echo(f" rate/second : {c.get('ratePerSecond')} deposit: {c.get('deposit')} withdrawn: {c.get('withdrawn')} halted: {c.get('halted')}")
453
+ click.echo(" Computed :")
454
+ click.echo(f" now : {comp.get('now')} remaining: {comp.get('remaining_seconds')}s")
455
+ click.echo(f" vested : {comp.get('vested_wei')} withdrawable: {comp.get('withdrawable_wei')}")
456
+ click.echo("─" * 60)
457
+ except Exception as e:
458
+ logger.error(f"Failed to fetch stream status: {e}")
459
+ raise click.Abort()
460
+
461
+
462
+ @vm_stream.command('inspect')
463
+ @click.option('--stream-id', type=int, required=True)
464
+ @click.option('--json', 'as_json', is_flag=True, help='Output in JSON format')
465
+ @async_command
466
+ async def stream_inspect(stream_id: int, as_json: bool):
467
+ """Inspect a stream directly on-chain (no provider required)."""
468
+ try:
469
+ from web3 import Web3
470
+ from golem_streaming_abi import STREAM_PAYMENT_ABI
471
+ w3 = Web3(Web3.HTTPProvider(config.polygon_rpc_url))
472
+ contract = w3.eth.contract(address=Web3.to_checksum_address(config.stream_payment_address), abi=STREAM_PAYMENT_ABI)
473
+ token, sender, recipient, startTime, stopTime, ratePerSecond, deposit, withdrawn, halted = contract.functions.streams(int(stream_id)).call()
474
+ now = int(w3.eth.get_block('latest')['timestamp'])
475
+ vested = max(min(now, int(stopTime)) - int(startTime), 0) * int(ratePerSecond)
476
+ withdrawable = max(int(vested) - int(withdrawn), 0)
477
+ remaining = max(int(stopTime) - now, 0)
478
+ out = {
479
+ "stream_id": int(stream_id),
480
+ "chain": {
481
+ "token": token,
482
+ "sender": sender,
483
+ "recipient": recipient,
484
+ "startTime": int(startTime),
485
+ "stopTime": int(stopTime),
486
+ "ratePerSecond": int(ratePerSecond),
487
+ "deposit": int(deposit),
488
+ "withdrawn": int(withdrawn),
489
+ "halted": bool(halted),
490
+ },
491
+ "computed": {
492
+ "now": now,
493
+ "remaining_seconds": remaining,
494
+ "vested_wei": int(vested),
495
+ "withdrawable_wei": int(withdrawable),
496
+ }
497
+ }
498
+ if as_json:
499
+ click.echo(json.dumps(out, indent=2))
500
+ else:
501
+ click.echo("\n" + "─" * 60)
502
+ click.echo(click.style(f" 🔎 On-chain Stream Inspect: {stream_id}", fg="blue", bold=True))
503
+ click.echo("─" * 60)
504
+ click.echo(f" recipient : {recipient}")
505
+ click.echo(f" startTime : {int(startTime)} stopTime: {int(stopTime)} now: {now} remaining: {remaining}s")
506
+ click.echo(f" rate/second : {int(ratePerSecond)} deposit: {int(deposit)} withdrawn: {int(withdrawn)} halted: {bool(halted)}")
507
+ click.echo(f" vested : {int(vested)} withdrawable: {int(withdrawable)}")
508
+ click.echo("─" * 60)
509
+ except Exception as e:
510
+ logger.error(f"Failed to inspect stream: {e}")
511
+ raise click.Abort()
512
+
513
+
387
514
  @cli.group()
388
515
  def wallet():
389
516
  """Wallet utilities (funding, balance)."""
requestor/config.py CHANGED
@@ -106,12 +106,12 @@ class RequestorConfig(BaseSettings):
106
106
  description="EVM RPC URL for streaming payments (L2 by default)"
107
107
  )
108
108
  stream_payment_address: str = Field(
109
- default="0x0000000000000000000000000000000000000000",
110
- description="Deployed StreamPayment contract address"
109
+ default="",
110
+ description="Deployed StreamPayment contract address (defaults to contracts/deployments/l2.json)"
111
111
  )
112
112
  glm_token_address: str = Field(
113
- default="0x0000000000000000000000000000000000000000",
114
- description="Token address (0x0 means native ETH)"
113
+ default="",
114
+ description="Token address (0x0 means native ETH). Defaults from l2.json"
115
115
  )
116
116
  # Faucet settings (L2 payments)
117
117
  l2_faucet_url: str = Field(
@@ -145,6 +145,52 @@ class RequestorConfig(BaseSettings):
145
145
  return os.environ[key]
146
146
  return v
147
147
 
148
+ @staticmethod
149
+ def _load_l2_deployment() -> tuple[str | None, str | None]:
150
+ try:
151
+ base = os.environ.get("GOLEM_DEPLOYMENTS_DIR")
152
+ if base:
153
+ path = Path(base) / "l2.json"
154
+ else:
155
+ # repo root assumption: ../../ relative to this file
156
+ path = Path(__file__).resolve().parents[2] / "contracts" / "deployments" / "l2.json"
157
+ if not path.exists():
158
+ # Try package resource fallback
159
+ try:
160
+ import importlib.resources as ir
161
+ with ir.files("requestor.data.deployments").joinpath("l2.json").open("r") as fh: # type: ignore[attr-defined]
162
+ import json as _json
163
+ data = _json.load(fh)
164
+ except Exception:
165
+ return None, None
166
+ else:
167
+ import json as _json
168
+ data = _json.loads(path.read_text())
169
+ sp = data.get("StreamPayment", {})
170
+ addr = sp.get("address")
171
+ token = sp.get("glmToken")
172
+ if isinstance(addr, str) and addr:
173
+ return addr, token or "0x0000000000000000000000000000000000000000"
174
+ except Exception:
175
+ pass
176
+ return None, None
177
+
178
+ @field_validator("stream_payment_address", mode='before')
179
+ @classmethod
180
+ def default_stream_addr(cls, v: str) -> str:
181
+ if v:
182
+ return v
183
+ addr, _ = RequestorConfig._load_l2_deployment()
184
+ return addr or "0x0000000000000000000000000000000000000000"
185
+
186
+ @field_validator("glm_token_address", mode='before')
187
+ @classmethod
188
+ def default_token_addr(cls, v: str) -> str:
189
+ if v:
190
+ return v
191
+ _, token = RequestorConfig._load_l2_deployment()
192
+ return token or "0x0000000000000000000000000000000000000000"
193
+
148
194
  # Base Directory
149
195
  base_dir: Path = Field(
150
196
  default_factory=lambda: Path.home() / ".golem" / "requestor",
@@ -0,0 +1,9 @@
1
+ {
2
+ "network": "l2",
3
+ "timestamp": "",
4
+ "StreamPayment": {
5
+ "address": "0x0281B792b5491E3548c8Fc17C24A2e0Cb99FbeC2",
6
+ "oracle": "0xDd329f6EDf93637634E8862d34c3909d298A7055",
7
+ "glmToken": "0x0000000000000000000000000000000000000000"
8
+ }
9
+ }
@@ -103,3 +103,13 @@ class ProviderClient:
103
103
  error_text = await response.text()
104
104
  raise Exception(f"Failed to get VM access info: {error_text}")
105
105
  return await response.json()
106
+
107
+ async def get_vm_stream_status(self, vm_id: str) -> Dict:
108
+ """Get on-chain stream status for a VM from provider."""
109
+ async with self.session.get(
110
+ f"{self.provider_url}/api/v1/vms/{vm_id}/stream"
111
+ ) as response:
112
+ if not response.ok:
113
+ error_text = await response.text()
114
+ raise Exception(f"Failed to get VM stream status: {error_text}")
115
+ return await response.json()