golem-vm-provider 0.1.42__py3-none-any.whl → 0.1.44__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.42
3
+ Version: 0.1.44
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
@@ -238,11 +238,12 @@ GOLEM_PROVIDER_NETWORK="testnet" # or "mainnet"
238
238
 
239
239
  ### Streaming Payments (Native ETH on L2)
240
240
 
241
- Enable on‑chain stream‑gated rentals funded in native ETH. Configure (env prefix `GOLEM_PROVIDER_`):
241
+ Enable on‑chain stream‑gated rentals funded in native ETH. By default, the provider auto‑loads the StreamPayment contract from `contracts/deployments/l2.json` and enables payments out of the box. Configure/override (env prefix `GOLEM_PROVIDER_`):
242
242
 
243
- - `POLYGON_RPC_URL` — EVM RPC URL (default points to L2: https://l2.holesky.golemdb.io/rpc)
244
- - `STREAM_PAYMENT_ADDRESS` — StreamPayment contract address; if non‑zero, VM creation requires a valid `stream_id`
245
- - `GLM_TOKEN_ADDRESS` — Token address; set to `0x0000000000000000000000000000000000000000` to indicate native ETH
243
+ - `POLYGON_RPC_URL` — EVM RPC URL (default L2 RPC)
244
+ - `STREAM_PAYMENT_ADDRESS` — StreamPayment address (defaults from `contracts/deployments/l2.json`)
245
+ - `GLM_TOKEN_ADDRESS` — Token address (defaults from `contracts/deployments/l2.json`; `0x0` means native ETH)
246
+ - Optional override of deployments directory: set `GOLEM_DEPLOYMENTS_DIR` to a folder containing `l2.json`.
246
247
 
247
248
  Optional background automation (all disabled by default):
248
249
 
@@ -255,7 +256,7 @@ Optional background automation (all disabled by default):
255
256
 
256
257
  Implementation notes:
257
258
 
258
- - The provider exposes `GET /api/v1/provider/info` returning `provider_id`, `stream_payment_address`, and `glm_token_address`. For ETH mode this field is the zero address (`0x000...000`). Requestors should prefer these values when opening streams.
259
+ - The provider exposes `GET /api/v1/provider/info` returning `provider_id`, `stream_payment_address`, and `glm_token_address`. Requestors should prefer these values when opening streams.
259
260
  - On successful VM creation with a valid `stream_id`, the provider persists a VM→stream mapping in `streams.json`. This enables the background monitor to stop VMs with low remaining runway and to withdraw vested funds according to configured intervals.
260
261
  - When a VM is deleted, the VM→stream mapping is cleaned up.
261
262
 
@@ -328,7 +329,44 @@ Response:
328
329
  ```
329
330
 
330
331
  Use this endpoint to discover the correct recipient for creating a GLM stream.
331
-
332
+
333
+ ### Payment Streams
334
+
335
+ - Get a VM’s stream status: `GET /api/v1/vms/{vm_id}/stream`
336
+ - List all mapped streams: `GET /api/v1/payments/streams`
337
+
338
+ Response (per stream):
339
+
340
+ ```json
341
+ {
342
+ "vm_id": "golem-my-webserver-20250219-130424",
343
+ "stream_id": 123,
344
+ "verified": true,
345
+ "reason": "ok",
346
+ "chain": {
347
+ "token": "0x0000000000000000000000000000000000000000",
348
+ "sender": "0x...",
349
+ "recipient": "0xProviderEthereumAddress",
350
+ "startTime": 1700000000,
351
+ "stopTime": 1700007200,
352
+ "ratePerSecond": 12345,
353
+ "deposit": 1000000000000000000,
354
+ "withdrawn": 0,
355
+ "halted": false
356
+ },
357
+ "computed": {
358
+ "now": 1700003600,
359
+ "remaining_seconds": 3600,
360
+ "vested_wei": 44442000,
361
+ "withdrawable_wei": 44442000
362
+ }
363
+ }
364
+ ```
365
+
366
+ Notes:
367
+ - Endpoints return 400 when streaming is disabled (zero `STREAM_PAYMENT_ADDRESS`).
368
+ - In development mode (`GOLEM_PROVIDER_ENVIRONMENT=development`) additional debug logs are emitted around stream verification and monitor ticks.
369
+
332
370
  ## Operations
333
371
 
334
372
  ### Starting the Provider
@@ -382,6 +420,22 @@ Defaults:
382
420
  - CAPTCHA: `https://cap.gobas.me/05381a2cef5e`
383
421
  - Override with env: `GOLEM_PROVIDER_L2_FAUCET_URL`, `GOLEM_PROVIDER_L2_CAPTCHA_URL`, `GOLEM_PROVIDER_L2_CAPTCHA_API_KEY`.
384
422
 
423
+ ### Streams (CLI)
424
+
425
+ - List all mapped streams with computed fields:
426
+
427
+ ```bash
428
+ poetry run golem-provider streams list
429
+ # or JSON
430
+ poetry run golem-provider streams list --json
431
+ ```
432
+
433
+ - Show one VM’s stream (VM id = `requestor_name`):
434
+
435
+ ```bash
436
+ poetry run golem-provider streams show <vm_id>
437
+ ```
438
+
385
439
  ### Resource Advertisement Flow
386
440
 
387
441
  ```mermaid
@@ -1,21 +1,22 @@
1
1
  provider/__init__.py,sha256=HO1fkPpZqPO3z8O8-eVIyx8xXSMIVuTR_b1YF0RtXOg,45
2
2
  provider/api/__init__.py,sha256=ssX1ugDqEPt8Fn04IymgmG-Ev8PiXLsCSaiZVvHQnec,344
3
- provider/api/models.py,sha256=_y3N9amTFaHfH7_3uZlvhPAoUA4HY-93f-BBuC4EoBM,3830
4
- provider/api/routes.py,sha256=DODju_SOgfkyeZv9zLTuAXbyvdaWRbU4kHZpON_GeMg,8288
5
- provider/config.py,sha256=u7Y0w9-s8iitQzQ3R0ztgYSfq_WeSRUYzBRTor8GAlY,22666
3
+ provider/api/models.py,sha256=CmfgXqSH3m0HLqY6JvUFI-2IrdGf3EhNKtZ5kbIAX-U,4304
4
+ provider/api/routes.py,sha256=Cv2oLkMDXY_65xlNSvmJAG1dzmlibxbPHy_J42kRJsA,12583
5
+ provider/config.py,sha256=nabZhzgUYE7GEdCou_uEzy3KGWhuqEhwKwasxYOEedI,24636
6
6
  provider/container.py,sha256=81x5LiA-qjYN1Uh_JdOxqvuIXiNDr9X3OXNN0VqYFCI,3681
7
+ provider/data/deployments/l2.json,sha256=XTNN2C5LkBfp4YbDKdUKfWMdp1fKnfv8D3TgcwVWxtQ,249
7
8
  provider/discovery/__init__.py,sha256=Y6o8RxGevBpuQS3k32y-zSVbP6HBXG3veBl9ElVPKaU,349
8
9
  provider/discovery/advertiser.py,sha256=o-LiDl1j0lXMUU0-zPe3qerjpoD2360EA60Y_V_VeBc,6571
9
- provider/discovery/golem_base_advertiser.py,sha256=0I6V73GL671c8mQNpb92sdyboXzbJuRJdv06QS7MuBQ,6942
10
+ provider/discovery/golem_base_advertiser.py,sha256=fPjBScWDDX5behuQfOFqGTp0tL-sXpLXGbnx3smRmfc,7243
10
11
  provider/discovery/golem_base_utils.py,sha256=xk7vznhMgzrn0AuGyk6-9N9ukp9oPdBbbk1RI-sVjp0,607
11
12
  provider/discovery/multi_advertiser.py,sha256=_J79wA1-XQ4GsLzt9KrKpWigGSGBqtut7DaocIk2fyE,991
12
13
  provider/discovery/resource_monitor.py,sha256=AmiEc7yBGEGXCunQ-QKmVgosDX3gOhK1Y58LJZXrwAs,949
13
14
  provider/discovery/resource_tracker.py,sha256=MP7IXd3aIMsjB4xz5Oj9zFDTEnvrnw-Cyxpl33xcJcc,6006
14
15
  provider/discovery/service.py,sha256=vX_mVSxvn3arnb2cKDM_SeJp1ZgPdImP2aUubeXgdRg,915
15
- provider/main.py,sha256=eS_rqvC9bCXdJfYg-sNYt02SGPdodgPGa350SWcd1os,14271
16
+ provider/main.py,sha256=TEk9J792BsSTQ0MstK_D6-Ls-9hAIDI6S4Cu6Qwu0dY,19012
16
17
  provider/network/port_verifier.py,sha256=3l6WNwBHydggJRFYkAsuBp1eCxaU619kjWuM-zSVj2o,13267
17
18
  provider/payments/blockchain_service.py,sha256=Qq01M7N_gzUidnXBngDcNoBRs_sHKaTpvSFKz6wEAMk,3363
18
- provider/payments/monitor.py,sha256=6zzW275ktSHcYiIXuyThr-UA-JR7VoHw2Hk1qAUCb-U,3058
19
+ provider/payments/monitor.py,sha256=TD88OrVfqGdzgfZkGNcCQMnGr0MLoj2p-bunDoalT8M,3814
19
20
  provider/payments/stream_map.py,sha256=qk6Y8hS72DplAifZ0ZMWPHBAyc_3IWIQyWUBuCU3_To,1191
20
21
  provider/security/ethereum.py,sha256=EwPZj4JR8OEpto6LhKjuuT3Z9pBX6P7-UQaqJtqFkYQ,1242
21
22
  provider/security/faucet.py,sha256=8T4lW1fVQgUk8EQILgbrr9UUosw9e7eA40tlZ2_KCPQ,4368
@@ -37,7 +38,7 @@ provider/vm/port_manager.py,sha256=iYSwjTjD_ziOhG8aI7juKHw1OwwRUTJQyQoRUNQvz9w,1
37
38
  provider/vm/provider.py,sha256=A7QN89EJjcSS40_SmKeinG1Jp_NGffJaLse-XdKciAs,1164
38
39
  provider/vm/proxy_manager.py,sha256=n4NTsyz2rtrvjtf_ceKBk-g2q_mzqPwruB1q7UlQVBc,14928
39
40
  provider/vm/service.py,sha256=Ki4SGNIZUq3XmaPMwAOoNzdZzKQsmFXid374wgjFPes,4636
40
- golem_vm_provider-0.1.42.dist-info/METADATA,sha256=YLJ8-2jYNdZH04C59FRgBTFamsaYmgddmXVctxKHoqY,15197
41
- golem_vm_provider-0.1.42.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
42
- golem_vm_provider-0.1.42.dist-info/entry_points.txt,sha256=5Jiie1dIXygmxmDW66bKKxQpmBLJ7leSKRrb8bkQALw,52
43
- golem_vm_provider-0.1.42.dist-info/RECORD,,
41
+ golem_vm_provider-0.1.44.dist-info/METADATA,sha256=G0kZDN-BIOXkC8yInfrrd8yOEMl5HWkA2mptK7Qf7R8,16589
42
+ golem_vm_provider-0.1.44.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
43
+ golem_vm_provider-0.1.44.dist-info/entry_points.txt,sha256=5Jiie1dIXygmxmDW66bKKxQpmBLJ7leSKRrb8bkQALw,52
44
+ golem_vm_provider-0.1.44.dist-info/RECORD,,
provider/api/models.py CHANGED
@@ -116,3 +116,31 @@ class ProviderInfoResponse(BaseModel):
116
116
  provider_id: str
117
117
  stream_payment_address: str
118
118
  glm_token_address: str
119
+
120
+
121
+ class StreamOnChain(BaseModel):
122
+ token: str
123
+ sender: str
124
+ recipient: str
125
+ startTime: int
126
+ stopTime: int
127
+ ratePerSecond: int
128
+ deposit: int
129
+ withdrawn: int
130
+ halted: bool
131
+
132
+
133
+ class StreamComputed(BaseModel):
134
+ now: int
135
+ remaining_seconds: int
136
+ vested_wei: int
137
+ withdrawable_wei: int
138
+
139
+
140
+ class StreamStatus(BaseModel):
141
+ vm_id: str
142
+ stream_id: int
143
+ chain: StreamOnChain
144
+ computed: StreamComputed
145
+ verified: bool
146
+ reason: str
provider/api/routes.py CHANGED
@@ -11,7 +11,7 @@ from ..container import Container
11
11
  from ..utils.logging import setup_logger
12
12
  from ..utils.ascii_art import vm_creation_animation, vm_status_change
13
13
  from ..vm.models import VMInfo, VMAccessInfo, VMConfig, VMResources, VMNotFoundError
14
- from .models import CreateVMRequest, ProviderInfoResponse
14
+ from .models import CreateVMRequest, ProviderInfoResponse, StreamStatus, StreamOnChain, StreamComputed
15
15
  from ..payments.blockchain_service import StreamPaymentReader
16
16
  from ..vm.service import VMService
17
17
  from ..vm.multipass_adapter import MultipassError
@@ -40,6 +40,17 @@ async def create_vm(
40
40
  raise HTTPException(status_code=400, detail="stream_id required when payments are enabled")
41
41
  reader = StreamPaymentReader(settings["POLYGON_RPC_URL"], settings["STREAM_PAYMENT_ADDRESS"])
42
42
  ok, reason = reader.verify_stream(int(request.stream_id), settings["PROVIDER_ID"])
43
+ try:
44
+ s = reader.get_stream(int(request.stream_id))
45
+ now = int(reader.web3.eth.get_block("latest")["timestamp"]) # type: ignore[attr-defined]
46
+ remaining = max(int(s["stopTime"]) - now, 0)
47
+ logger.info(
48
+ f"💸 Stream check id={int(request.stream_id)} ok={ok} reason='{reason}' "
49
+ f"start={s['startTime']} stop={s['stopTime']} rate={s['ratePerSecond']} deposit={s['deposit']} withdrawn={s['withdrawn']} remaining={remaining}s"
50
+ )
51
+ except Exception:
52
+ # Best-effort logging; creation will continue/fail based on ok
53
+ pass
43
54
  if not ok:
44
55
  raise HTTPException(status_code=400, detail=f"invalid stream: {reason}")
45
56
 
@@ -201,3 +212,72 @@ async def provider_info(settings: Settings = Depends(Provide[Container.config]))
201
212
  stream_payment_address=settings["STREAM_PAYMENT_ADDRESS"],
202
213
  glm_token_address=settings["GLM_TOKEN_ADDRESS"],
203
214
  )
215
+
216
+
217
+ @router.get("/vms/{requestor_name}/stream", response_model=StreamStatus)
218
+ @inject
219
+ async def get_vm_stream_status(
220
+ requestor_name: str,
221
+ settings: Settings = Depends(Provide[Container.config]),
222
+ stream_map = Depends(Provide[Container.stream_map]),
223
+ ) -> StreamStatus:
224
+ """Return on-chain stream status for a VM (if mapped)."""
225
+ if not settings["STREAM_PAYMENT_ADDRESS"] or settings["STREAM_PAYMENT_ADDRESS"] == "0x0000000000000000000000000000000000000000":
226
+ raise HTTPException(status_code=400, detail="streaming payments not enabled on this provider")
227
+ stream_id = await stream_map.get(requestor_name)
228
+ if stream_id is None:
229
+ raise HTTPException(status_code=404, detail="no stream mapped for this VM")
230
+ reader = StreamPaymentReader(settings["POLYGON_RPC_URL"], settings["STREAM_PAYMENT_ADDRESS"])
231
+ try:
232
+ s = reader.get_stream(int(stream_id))
233
+ except Exception as e:
234
+ raise HTTPException(status_code=502, detail=f"stream lookup failed: {e}")
235
+ ok, reason = reader.verify_stream(int(stream_id), settings["PROVIDER_ID"])
236
+ now = int(reader.web3.eth.get_block("latest")["timestamp"]) # type: ignore[attr-defined]
237
+ vested = max(min(now, int(s["stopTime"])) - int(s["startTime"]), 0) * int(s["ratePerSecond"]) # type: ignore[operator]
238
+ withdrawable = max(int(vested) - int(s["withdrawn"]), 0)
239
+ remaining = max(int(s["stopTime"]) - now, 0)
240
+ return StreamStatus(
241
+ vm_id=requestor_name,
242
+ stream_id=int(stream_id),
243
+ chain=StreamOnChain(**s),
244
+ computed=StreamComputed(now=now, remaining_seconds=remaining, vested_wei=int(vested), withdrawable_wei=int(withdrawable)),
245
+ verified=bool(ok),
246
+ reason=str(reason),
247
+ )
248
+
249
+
250
+ @router.get("/payments/streams", response_model=List[StreamStatus])
251
+ @inject
252
+ async def list_stream_statuses(
253
+ settings: Settings = Depends(Provide[Container.config]),
254
+ stream_map = Depends(Provide[Container.stream_map]),
255
+ ) -> List[StreamStatus]:
256
+ """List stream status for all mapped VMs."""
257
+ if not settings["STREAM_PAYMENT_ADDRESS"] or settings["STREAM_PAYMENT_ADDRESS"] == "0x0000000000000000000000000000000000000000":
258
+ raise HTTPException(status_code=400, detail="streaming payments not enabled on this provider")
259
+ reader = StreamPaymentReader(settings["POLYGON_RPC_URL"], settings["STREAM_PAYMENT_ADDRESS"])
260
+ items = await stream_map.all_items()
261
+ now = int(reader.web3.eth.get_block("latest")["timestamp"]) if items else 0 # type: ignore[attr-defined]
262
+ resp: List[StreamStatus] = []
263
+ for vm_id, stream_id in items.items():
264
+ try:
265
+ s = reader.get_stream(int(stream_id))
266
+ ok, reason = reader.verify_stream(int(stream_id), settings["PROVIDER_ID"])
267
+ vested = max(min(now, int(s["stopTime"])) - int(s["startTime"]), 0) * int(s["ratePerSecond"]) # type: ignore[operator]
268
+ withdrawable = max(int(vested) - int(s["withdrawn"]), 0)
269
+ remaining = max(int(s["stopTime"]) - now, 0)
270
+ resp.append(
271
+ StreamStatus(
272
+ vm_id=vm_id,
273
+ stream_id=int(stream_id),
274
+ chain=StreamOnChain(**s),
275
+ computed=StreamComputed(now=now, remaining_seconds=remaining, vested_wei=int(vested), withdrawable_wei=int(withdrawable)),
276
+ verified=bool(ok),
277
+ reason=str(reason),
278
+ )
279
+ )
280
+ except Exception as e:
281
+ logger.warning(f"stream {stream_id} lookup failed: {e}")
282
+ continue
283
+ return resp
provider/config.py CHANGED
@@ -1,4 +1,5 @@
1
1
  import os
2
+ import json
2
3
  from pathlib import Path
3
4
  from typing import Optional
4
5
  import uuid
@@ -141,12 +142,12 @@ class Settings(BaseSettings):
141
142
  description="EVM RPC URL for streaming payments (L2 by default)"
142
143
  )
143
144
  STREAM_PAYMENT_ADDRESS: str = Field(
144
- default="0x0000000000000000000000000000000000000000",
145
- description="Deployed StreamPayment contract address"
145
+ default="",
146
+ description="Deployed StreamPayment contract address (defaults to contracts/deployments/l2.json)"
146
147
  )
147
148
  GLM_TOKEN_ADDRESS: str = Field(
148
- default="0x0000000000000000000000000000000000000000",
149
- description="Token address (0x0 means native ETH)"
149
+ default="",
150
+ description="Token address (0x0 means native ETH). Defaults from l2.json"
150
151
  )
151
152
  STREAM_MIN_REMAINING_SECONDS: int = Field(
152
153
  default=3600,
@@ -201,6 +202,55 @@ class Settings(BaseSettings):
201
202
  return os.environ[key]
202
203
  return v
203
204
 
205
+ @staticmethod
206
+ def _load_l2_deployment() -> tuple[str | None, str | None]:
207
+ """Try to load default StreamPayment + token from contracts/deployments/l2.json.
208
+
209
+ Returns (stream_payment_address, glm_token_address) or (None, None) if not found.
210
+ """
211
+ try:
212
+ # Allow override via env
213
+ base = os.environ.get("GOLEM_DEPLOYMENTS_DIR")
214
+ if base:
215
+ path = Path(base) / "l2.json"
216
+ else:
217
+ # repo root = ../../ from this file
218
+ path = Path(__file__).resolve().parents[2] / "contracts" / "deployments" / "l2.json"
219
+ if not path.exists():
220
+ # Try package resource fallback
221
+ try:
222
+ import importlib.resources as ir
223
+ with ir.files("provider.data.deployments").joinpath("l2.json").open("r") as fh: # type: ignore[attr-defined]
224
+ data = json.load(fh)
225
+ except Exception:
226
+ return None, None
227
+ else:
228
+ data = json.loads(path.read_text())
229
+ sp = data.get("StreamPayment", {})
230
+ addr = sp.get("address")
231
+ token = sp.get("glmToken")
232
+ if isinstance(addr, str) and addr:
233
+ return addr, token or "0x0000000000000000000000000000000000000000"
234
+ except Exception:
235
+ pass
236
+ return None, None
237
+
238
+ @field_validator("STREAM_PAYMENT_ADDRESS", mode='before')
239
+ @classmethod
240
+ def default_stream_addr(cls, v: str) -> str:
241
+ if v:
242
+ return v
243
+ addr, _ = Settings._load_l2_deployment()
244
+ return addr or "0x0000000000000000000000000000000000000000"
245
+
246
+ @field_validator("GLM_TOKEN_ADDRESS", mode='before')
247
+ @classmethod
248
+ def default_token_addr(cls, v: str) -> str:
249
+ if v:
250
+ return v
251
+ _, token = Settings._load_l2_deployment()
252
+ return token or "0x0000000000000000000000000000000000000000"
253
+
204
254
  # VM Settings
205
255
  MAX_VMS: int = 10
206
256
  DEFAULT_VM_IMAGE: str = "ubuntu:24.04"
@@ -0,0 +1,9 @@
1
+ {
2
+ "network": "l2",
3
+ "timestamp": "",
4
+ "StreamPayment": {
5
+ "address": "0x0281B792b5491E3548c8Fc17C24A2e0Cb99FbeC2",
6
+ "oracle": "0xDd329f6EDf93637634E8862d34c3909d298A7055",
7
+ "glmToken": "0x0000000000000000000000000000000000000000"
8
+ }
9
+ }
@@ -64,6 +64,7 @@ class GolemBaseAdvertiser(Advertiser):
64
64
  try:
65
65
  existing_keys = await get_provider_entity_keys(self.client, settings.PROVIDER_ID)
66
66
 
67
+ # String annotations (metadata + prices as strings; on-chain numeric annotations must be ints)
67
68
  string_annotations = [
68
69
  Annotation(key="golem_type", value="provider"),
69
70
  Annotation(key="golem_network", value=settings.NETWORK),
@@ -72,17 +73,19 @@ class GolemBaseAdvertiser(Advertiser):
72
73
  Annotation(key="golem_country", value=settings.PROVIDER_COUNTRY),
73
74
  Annotation(key="golem_provider_name", value=settings.PROVIDER_NAME),
74
75
  Annotation(key="golem_price_currency", value="USD/GLM"),
76
+ # Prices must be strings to avoid RLP sedes errors (ints only allowed for numeric annotations)
77
+ Annotation(key="golem_price_usd_core_month", value=str(float(settings.PRICE_USD_PER_CORE_MONTH))),
78
+ Annotation(key="golem_price_usd_ram_gb_month", value=str(float(settings.PRICE_USD_PER_GB_RAM_MONTH))),
79
+ Annotation(key="golem_price_usd_storage_gb_month", value=str(float(settings.PRICE_USD_PER_GB_STORAGE_MONTH))),
80
+ Annotation(key="golem_price_glm_core_month", value=str(float(settings.PRICE_GLM_PER_CORE_MONTH))),
81
+ Annotation(key="golem_price_glm_ram_gb_month", value=str(float(settings.PRICE_GLM_PER_GB_RAM_MONTH))),
82
+ Annotation(key="golem_price_glm_storage_gb_month", value=str(float(settings.PRICE_GLM_PER_GB_STORAGE_MONTH))),
75
83
  ]
84
+ # Numeric annotations: strictly integers
76
85
  numeric_annotations = [
77
86
  Annotation(key="golem_cpu", value=resources["cpu"]),
78
87
  Annotation(key="golem_memory", value=resources["memory"]),
79
88
  Annotation(key="golem_storage", value=resources["storage"]),
80
- Annotation(key="golem_price_usd_core_month", value=float(settings.PRICE_USD_PER_CORE_MONTH)),
81
- Annotation(key="golem_price_usd_ram_gb_month", value=float(settings.PRICE_USD_PER_GB_RAM_MONTH)),
82
- Annotation(key="golem_price_usd_storage_gb_month", value=float(settings.PRICE_USD_PER_GB_STORAGE_MONTH)),
83
- Annotation(key="golem_price_glm_core_month", value=float(settings.PRICE_GLM_PER_CORE_MONTH)),
84
- Annotation(key="golem_price_glm_ram_gb_month", value=float(settings.PRICE_GLM_PER_GB_RAM_MONTH)),
85
- Annotation(key="golem_price_glm_storage_gb_month", value=float(settings.PRICE_GLM_PER_GB_STORAGE_MONTH)),
86
89
  ]
87
90
 
88
91
  if len(existing_keys) > 1:
provider/main.py CHANGED
@@ -123,8 +123,10 @@ except ImportError:
123
123
  cli = typer.Typer()
124
124
  pricing_app = typer.Typer(help="Configure USD pricing; auto-converts to GLM.")
125
125
  wallet_app = typer.Typer(help="Wallet utilities (funding, balance)")
126
+ streams_app = typer.Typer(help="Inspect payment streams")
126
127
  cli.add_typer(pricing_app, name="pricing")
127
128
  cli.add_typer(wallet_app, name="wallet")
129
+ cli.add_typer(streams_app, name="streams")
128
130
 
129
131
  def print_version(ctx: typer.Context, value: bool):
130
132
  if not value:
@@ -185,6 +187,108 @@ def wallet_faucet_l2():
185
187
  print(f"Error: {e}")
186
188
  raise typer.Exit(code=1)
187
189
 
190
+
191
+ @streams_app.command("list")
192
+ def streams_list(json_out: bool = typer.Option(False, "--json", help="Output in JSON")):
193
+ """List all mapped streams with computed status."""
194
+ from .container import Container
195
+ from .config import settings
196
+ from .payments.blockchain_service import StreamPaymentReader
197
+ import json as _json
198
+ try:
199
+ if not settings.STREAM_PAYMENT_ADDRESS or settings.STREAM_PAYMENT_ADDRESS == "0x0000000000000000000000000000000000000000":
200
+ print("Streaming payments are disabled on this provider.")
201
+ raise typer.Exit(code=1)
202
+ c = Container()
203
+ c.config.from_pydantic(settings)
204
+ stream_map = c.stream_map()
205
+ reader = StreamPaymentReader(settings.POLYGON_RPC_URL, settings.STREAM_PAYMENT_ADDRESS)
206
+ items = asyncio.run(stream_map.all_items())
207
+ now = int(reader.web3.eth.get_block("latest")["timestamp"]) if items else 0
208
+ rows = []
209
+ for vm_id, stream_id in items.items():
210
+ try:
211
+ s = reader.get_stream(int(stream_id))
212
+ vested = max(min(now, int(s["stopTime"])) - int(s["startTime"]), 0) * int(s["ratePerSecond"]) # type: ignore
213
+ withdrawable = max(int(vested) - int(s["withdrawn"]), 0)
214
+ remaining = max(int(s["stopTime"]) - now, 0)
215
+ ok, reason = reader.verify_stream(int(stream_id), settings.PROVIDER_ID)
216
+ rows.append({
217
+ "vm_id": vm_id,
218
+ "stream_id": int(stream_id),
219
+ "recipient": s["recipient"],
220
+ "start": int(s["startTime"]),
221
+ "stop": int(s["stopTime"]),
222
+ "rate": int(s["ratePerSecond"]),
223
+ "deposit": int(s["deposit"]),
224
+ "withdrawn": int(s["withdrawn"]),
225
+ "remaining": remaining,
226
+ "verified": bool(ok),
227
+ "reason": reason,
228
+ "withdrawable": int(withdrawable),
229
+ })
230
+ except Exception as e:
231
+ rows.append({"vm_id": vm_id, "stream_id": int(stream_id), "error": str(e)})
232
+ if json_out:
233
+ print(_json.dumps({"streams": rows}, indent=2))
234
+ return
235
+ if not rows:
236
+ print("No streams mapped.")
237
+ return
238
+ print("\nStreams (VM → stream_id, remaining s, verified):")
239
+ for r in rows:
240
+ if "error" in r:
241
+ print(f"- {r['vm_id']}: {r['stream_id']} ERROR: {r['error']}")
242
+ else:
243
+ print(f"- {r['vm_id']}: {r['stream_id']} remaining={r['remaining']}s verified={r['verified']} reason={r['reason']} withdrawable={r['withdrawable']}")
244
+ except Exception as e:
245
+ print(f"Error: {e}")
246
+ raise typer.Exit(code=1)
247
+
248
+
249
+ @streams_app.command("show")
250
+ def streams_show(vm_id: str = typer.Argument(..., help="VM id (requestor_name)"), json_out: bool = typer.Option(False, "--json")):
251
+ """Show a single VM's stream status."""
252
+ from .container import Container
253
+ from .config import settings
254
+ from .payments.blockchain_service import StreamPaymentReader
255
+ import json as _json
256
+ try:
257
+ c = Container()
258
+ c.config.from_pydantic(settings)
259
+ stream_map = c.stream_map()
260
+ sid = asyncio.run(stream_map.get(vm_id))
261
+ if sid is None:
262
+ print("No stream mapped for this VM.")
263
+ raise typer.Exit(code=1)
264
+ reader = StreamPaymentReader(settings.POLYGON_RPC_URL, settings.STREAM_PAYMENT_ADDRESS)
265
+ s = reader.get_stream(int(sid))
266
+ now = int(reader.web3.eth.get_block("latest")["timestamp"]) # type: ignore
267
+ vested = max(min(now, int(s["stopTime"])) - int(s["startTime"]), 0) * int(s["ratePerSecond"]) # type: ignore
268
+ withdrawable = max(int(vested) - int(s["withdrawn"]), 0)
269
+ remaining = max(int(s["stopTime"]) - now, 0)
270
+ ok, reason = reader.verify_stream(int(sid), settings.PROVIDER_ID)
271
+ out = {
272
+ "vm_id": vm_id,
273
+ "stream_id": int(sid),
274
+ "chain": s,
275
+ "computed": {
276
+ "now": now,
277
+ "remaining_seconds": remaining,
278
+ "vested_wei": int(vested),
279
+ "withdrawable_wei": int(withdrawable),
280
+ },
281
+ "verified": bool(ok),
282
+ "reason": reason,
283
+ }
284
+ if json_out:
285
+ print(_json.dumps(out, indent=2))
286
+ else:
287
+ print(f"VM {vm_id}: stream {sid} remaining={remaining}s verified={ok} withdrawable={withdrawable}")
288
+ except Exception as e:
289
+ print(f"Error: {e}")
290
+ raise typer.Exit(code=1)
291
+
188
292
  @cli.command()
189
293
  def start(
190
294
  no_verify_port: bool = typer.Option(False, "--no-verify-port", help="Skip provider port verification."),
@@ -17,6 +17,10 @@ class StreamMonitor:
17
17
 
18
18
  def start(self):
19
19
  if self.settings.STREAM_MONITOR_ENABLED or self.settings.STREAM_WITHDRAW_ENABLED:
20
+ logger.info(
21
+ f"⏱️ Stream monitor enabled (check={self.settings.STREAM_MONITOR_ENABLED}, withdraw={self.settings.STREAM_WITHDRAW_ENABLED}) "
22
+ f"interval={self.settings.STREAM_MONITOR_INTERVAL_SECONDS}s"
23
+ )
20
24
  self._task = asyncio.create_task(self._run(), name="stream-monitor")
21
25
 
22
26
  async def stop(self):
@@ -34,6 +38,7 @@ class StreamMonitor:
34
38
  await asyncio.sleep(self.settings.STREAM_MONITOR_INTERVAL_SECONDS)
35
39
  items = await self.stream_map.all_items()
36
40
  now = int(self.reader.web3.eth.get_block("latest")["timestamp"]) if items else 0
41
+ logger.debug(f"stream monitor tick: {len(items)} streams, now={now}")
37
42
  for vm_id, stream_id in items.items():
38
43
  try:
39
44
  s = self.reader.get_stream(stream_id)
@@ -42,6 +47,10 @@ class StreamMonitor:
42
47
  continue
43
48
  # Stop VM if remaining runway < threshold
44
49
  remaining = max(s["stopTime"] - now, 0)
50
+ logger.debug(
51
+ f"stream {stream_id} for VM {vm_id}: start={s['startTime']} stop={s['stopTime']} "
52
+ f"rate={s['ratePerSecond']} withdrawn={s['withdrawn']} halted={s['halted']} remaining={remaining}s"
53
+ )
45
54
  if self.settings.STREAM_MONITOR_ENABLED and remaining < self.settings.STREAM_MIN_REMAINING_SECONDS:
46
55
  logger.info(f"Stopping VM {vm_id} due to low stream runway ({remaining}s)")
47
56
  try:
@@ -52,6 +61,7 @@ class StreamMonitor:
52
61
  if self.settings.STREAM_WITHDRAW_ENABLED and self.client:
53
62
  vested = max(min(now, s["stopTime"]) - s["startTime"], 0) * s["ratePerSecond"]
54
63
  withdrawable = max(vested - s["withdrawn"], 0)
64
+ logger.debug(f"withdraw check stream {stream_id}: vested={vested} withdrawable={withdrawable}")
55
65
  # Enforce a minimum interval between withdrawals
56
66
  if withdrawable >= self.settings.STREAM_MIN_WITHDRAW_WEI and (
57
67
  now - last_withdraw >= self.settings.STREAM_WITHDRAW_INTERVAL_SECONDS