golem-vm-provider 0.1.53__py3-none-any.whl → 0.1.55__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.53
3
+ Version: 0.1.55
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
@@ -45,7 +45,35 @@ Description-Content-Type: text/markdown
45
45
 
46
46
  # VM on Golem Provider Node
47
47
 
48
- The Provider Node enables participation in the Golem Network by managing virtual machines and computing resources. It handles VM lifecycle management, resource allocation, network proxying, and automated discovery advertisement.
48
+ Earn by renting out your machine’s compute like Airbnb for servers. The Provider service runs VMs for requestors, verifies payments via streaming, and lets you withdraw earnings.
49
+
50
+ ## Quick Start (Host and Earn)
51
+
52
+ 1) Install (Python 3.11+ recommended):
53
+
54
+ ```bash
55
+ pip install golem-vm-provider
56
+ ```
57
+
58
+ 2) Start the provider (testnet by default is fine):
59
+
60
+ ```bash
61
+ golem-provider start --network testnet
62
+ ```
63
+
64
+ 3) Set pricing in USD (GLM rates auto‑compute):
65
+
66
+ ```bash
67
+ golem-provider pricing set --usd-per-core 12 --usd-per-mem 4 --usd-per-disk 0.1
68
+ ```
69
+
70
+ 4) On testnets, optionally fund gas for withdrawals:
71
+
72
+ ```bash
73
+ golem-provider wallet faucet-l2
74
+ ```
75
+
76
+ You are now discoverable to requestors and will earn as your VMs run.
49
77
 
50
78
  ## System Architecture
51
79
 
@@ -178,26 +206,26 @@ sequenceDiagram
178
206
  - Clean connection handling
179
207
  - Automatic proxy cleanup
180
208
 
181
- ## Installation
209
+ ## Installation (from source / development)
182
210
 
183
211
  1. Prerequisites:
212
+ - Python 3.11+
213
+ - Multipass
214
+ - Poetry (for development)
184
215
 
185
- - Python 3.9+
186
- - Multipass
187
- - Poetry
216
+ 2. Install from source:
188
217
 
189
- 2. Install dependencies:
218
+ ```bash
219
+ cd provider-server
220
+ poetry install
221
+ ```
190
222
 
191
- ```bash
192
- cd provider-server
193
- poetry install
194
- ```
223
+ 3. Local environment (optional):
195
224
 
196
- 3. Configure environment:
197
- ```bash
198
- cp .env.example .env
199
- # Edit .env with your settings
200
- ```
225
+ ```bash
226
+ cp .env.example .env
227
+ # Edit .env to tweak defaults if needed
228
+ ```
201
229
 
202
230
  ## Configuration
203
231
 
@@ -227,9 +255,9 @@ GOLEM_PROVIDER_PORT_RANGE_START={start_port} # Default: 50800
227
255
  GOLEM_PROVIDER_PORT_RANGE_END={end_port} # Default: 50900
228
256
  GOLEM_PROVIDER_PUBLIC_IP="auto"
229
257
 
230
- # Discovery Settings
231
- GOLEM_PROVIDER_DISCOVERY_URL="http://discovery.golem.network:9001"
232
- GOLEM_PROVIDER_ADVERTISEMENT_INTERVAL=240
258
+ # Legacy discovery (optional; not required in normal operation)
259
+ # GOLEM_PROVIDER_DISCOVERY_URL="http://discovery.golem.network:9001"
260
+ # GOLEM_PROVIDER_ADVERTISEMENT_INTERVAL=240
233
261
 
234
262
  # Network Selection
235
263
  # Adds an annotation to on-chain advertisements and can be used by requestors to filter
@@ -266,7 +294,7 @@ When enabled, the provider verifies each VM creation request’s `stream_id` and
266
294
  - deposit is zero, stream not started, or stream halted
267
295
  - (Optional) remaining runway < `STREAM_MIN_REMAINING_SECONDS`
268
296
 
269
- ## API Reference
297
+ ## API Reference (for integrators)
270
298
 
271
299
  ### Create VM
272
300
 
@@ -372,11 +400,11 @@ Notes:
372
400
  ### Starting the Provider
373
401
 
374
402
  ```bash
375
- # To run in production mode
376
- poetry run golem-provider start
403
+ # Production mode
404
+ golem-provider start
377
405
 
378
- # To run in development mode, set the environment and optionally network
379
- GOLEM_PROVIDER_ENVIRONMENT=development poetry run golem-provider start --network testnet
406
+ # Development mode with extra logs and reload
407
+ GOLEM_PROVIDER_ENVIRONMENT=development golem-provider start --network testnet
380
408
  ```
381
409
 
382
410
  ### Mode vs. Network
@@ -390,6 +418,9 @@ GOLEM_PROVIDER_ENVIRONMENT=development poetry run golem-provider start --network
390
418
  - Pair with appropriate RPC envs (`GOLEM_PROVIDER_GOLEM_BASE_RPC_URL`, `GOLEM_PROVIDER_GOLEM_BASE_WS_URL`).
391
419
  - Does not change dev ergonomics (logging, reload, or port verification behavior).
392
420
 
421
+ - Payments Network (`GOLEM_PROVIDER_PAYMENTS_NETWORK`)
422
+ - Selects the payments chain profile (e.g., `l2.holesky`, `mainnet`). Determines default payments RPC, faucet enablement, and symbols.
423
+
393
424
  Common setups:
394
425
  - Local dev on testnet: `GOLEM_PROVIDER_ENVIRONMENT=development` plus `--network testnet`.
395
426
  - Staging on testnet: keep `ENVIRONMENT=production`, set `--network testnet` and testnet RPCs.
@@ -406,17 +437,20 @@ The provider will:
406
437
  4. Begin resource advertisement
407
438
  5. Listen for VM requests
408
439
 
440
+ Notes:
441
+ - Advertisements include both `golem_network` (testnet/mainnet) and `golem_payments_network` (e.g., `l2.holesky`). Requestors default to matching both; they can list all payments networks with a CLI flag.
442
+
409
443
  ### Faucet
410
444
 
411
445
  - L3 (Golem Base adverts): provider auto-requests funds on startup from `FAUCET_URL` (defaults to EthWarsaw Holesky) protected by CAPTCHA at `CAPTCHA_URL/05381a2cef5e`.
412
- - L2 (payments): Use the CLI to request native ETH:
446
+ - L2 (payments): Use the CLI to request native ETH (enabled only on testnet profiles):
413
447
 
414
448
  ```bash
415
- poetry run golem-provider wallet faucet-l2
449
+ golem-provider wallet faucet-l2
416
450
  ```
417
451
 
418
452
  Defaults:
419
- - L2 faucet: `https://l2.holesky.golemdb.io/faucet`
453
+ - Faucet URL and enablement come from the active payments profile. On `mainnet` (or other profiles without faucet) the command is disabled.
420
454
  - CAPTCHA: `https://cap.gobas.me/05381a2cef5e`
421
455
  - Override with env: `GOLEM_PROVIDER_L2_FAUCET_URL`, `GOLEM_PROVIDER_L2_CAPTCHA_URL`, `GOLEM_PROVIDER_L2_CAPTCHA_API_KEY`.
422
456
 
@@ -460,10 +494,10 @@ Configure monitor and withdraw via CLI:
460
494
 
461
495
  ```bash
462
496
  # Set monitor to require 1h remaining, check every 30s
463
- poetry run golem-provider config monitor --enable true --interval 30 --min-remaining 3600
497
+ golem-provider config monitor --enable true --interval 30 --min-remaining 3600
464
498
 
465
499
  # Enable auto-withdraw every 15 minutes when >= 1e15 wei
466
- poetry run golem-provider config withdraw --enable true --interval 900 --min-wei 1000000000000000
500
+ golem-provider config withdraw --enable true --interval 900 --min-wei 1000000000000000
467
501
  ```
468
502
 
469
503
  ### Resource Advertisement Flow
@@ -476,6 +510,7 @@ sequenceDiagram
476
510
  participant DS as Discovery Service
477
511
 
478
512
  P->>RT: Initialize
513
+ P->>RT: Sync with existing VMs
479
514
  RT->>AD: Register Callback
480
515
  loop Every 4 minutes
481
516
  AD->>RT: Get Resources
@@ -485,6 +520,8 @@ sequenceDiagram
485
520
  end
486
521
  ```
487
522
 
523
+ On startup, the provider syncs the resource tracker with all VMs currently running on the host (via Multipass). This ensures advertisements reflect already-allocated CPU, RAM, and storage after restarts, preventing false “outdated advertisement” updates when existing VMs are consuming resources. The sync is based on actual VMs present, independent of any still-open payment streams.
524
+
488
525
  ### Monitoring
489
526
 
490
527
  The provider includes comprehensive logging:
@@ -2,26 +2,26 @@ provider/__init__.py,sha256=HO1fkPpZqPO3z8O8-eVIyx8xXSMIVuTR_b1YF0RtXOg,45
2
2
  provider/api/__init__.py,sha256=ssX1ugDqEPt8Fn04IymgmG-Ev8PiXLsCSaiZVvHQnec,344
3
3
  provider/api/models.py,sha256=CmfgXqSH3m0HLqY6JvUFI-2IrdGf3EhNKtZ5kbIAX-U,4304
4
4
  provider/api/routes.py,sha256=RaOhdUZLJVmCHFWHyhYF9kdBmsFSe5rThIYsW6meMrQ,13194
5
- provider/config.py,sha256=GsrhbZYvMla1WWSHUg6q4wn0_SK3zgSPtlWGve_H2e8,24932
5
+ provider/config.py,sha256=IDeAYQ4z8oaT5HcG9jFQhSZrLlLU6wMTGDTbSxK6FSc,28901
6
6
  provider/container.py,sha256=81x5LiA-qjYN1Uh_JdOxqvuIXiNDr9X3OXNN0VqYFCI,3681
7
7
  provider/data/deployments/l2.json,sha256=XTNN2C5LkBfp4YbDKdUKfWMdp1fKnfv8D3TgcwVWxtQ,249
8
8
  provider/discovery/__init__.py,sha256=Y6o8RxGevBpuQS3k32y-zSVbP6HBXG3veBl9ElVPKaU,349
9
9
  provider/discovery/advertiser.py,sha256=o-LiDl1j0lXMUU0-zPe3qerjpoD2360EA60Y_V_VeBc,6571
10
- provider/discovery/golem_base_advertiser.py,sha256=fPjBScWDDX5behuQfOFqGTp0tL-sXpLXGbnx3smRmfc,7243
10
+ provider/discovery/golem_base_advertiser.py,sha256=A8bg40b2Ua7PIjx3Y8-SC0s-dUUPWxaiQCzr6AcpYaQ,7334
11
11
  provider/discovery/golem_base_utils.py,sha256=xk7vznhMgzrn0AuGyk6-9N9ukp9oPdBbbk1RI-sVjp0,607
12
12
  provider/discovery/multi_advertiser.py,sha256=_J79wA1-XQ4GsLzt9KrKpWigGSGBqtut7DaocIk2fyE,991
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=DJPVjvrTifFIw1l6jk0bwrOt35pmzmdVynv4A1fp9Y0,32137
16
+ provider/main.py,sha256=_j92g56B-d8CE09Ugv0fqWVMi5jw_iuTrysxSw7845A,32309
17
17
  provider/network/port_verifier.py,sha256=3l6WNwBHydggJRFYkAsuBp1eCxaU619kjWuM-zSVj2o,13267
18
18
  provider/payments/blockchain_service.py,sha256=4GrzDKwCSUVoENqjD4RLyJ0qwBOJKMyVk5Li-XNsyTc,3567
19
- provider/payments/monitor.py,sha256=Rw17zYsxZre0zU6R0oeRNvVIzMdXLsgoUvSPHpJy6I0,4488
19
+ provider/payments/monitor.py,sha256=seo8vE622IdbcRE3x69IpvHn2mel_tlMNGt_DxOIoww,5386
20
20
  provider/payments/stream_map.py,sha256=qk6Y8hS72DplAifZ0ZMWPHBAyc_3IWIQyWUBuCU3_To,1191
21
21
  provider/security/ethereum.py,sha256=EwPZj4JR8OEpto6LhKjuuT3Z9pBX6P7-UQaqJtqFkYQ,1242
22
22
  provider/security/faucet.py,sha256=8T4lW1fVQgUk8EQILgbrr9UUosw9e7eA40tlZ2_KCPQ,4368
23
- provider/security/l2_faucet.py,sha256=qp6Q2F44jsX5w8zZ14gzILesiGSxJhC1E_VVgqerBgY,2388
24
- provider/service.py,sha256=IIjeSM9T4r616nBRnxCUum_sgbyRusMMcja3yQd8zQI,3383
23
+ provider/security/l2_faucet.py,sha256=yRV4xdPBgU8-LDTLqtuAijfgIoe2kYxvXqJLxFd-BVI,2662
24
+ provider/service.py,sha256=hlQn0woppsYFHZDMEgq-40cOjmiPWruiWLy_dQvaCRU,6859
25
25
  provider/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
26
26
  provider/utils/ascii_art.py,sha256=ykBFsztk57GIiz1NJ-EII5UvN74iECqQL4h9VmiW6Z8,3161
27
27
  provider/utils/logging.py,sha256=VV3oTYSRT8hUejtXLuua1M6kCHmIJgPspIkzsUVhYW0,1920
@@ -39,7 +39,7 @@ provider/vm/port_manager.py,sha256=iYSwjTjD_ziOhG8aI7juKHw1OwwRUTJQyQoRUNQvz9w,1
39
39
  provider/vm/provider.py,sha256=A7QN89EJjcSS40_SmKeinG1Jp_NGffJaLse-XdKciAs,1164
40
40
  provider/vm/proxy_manager.py,sha256=n4NTsyz2rtrvjtf_ceKBk-g2q_mzqPwruB1q7UlQVBc,14928
41
41
  provider/vm/service.py,sha256=Ki4SGNIZUq3XmaPMwAOoNzdZzKQsmFXid374wgjFPes,4636
42
- golem_vm_provider-0.1.53.dist-info/METADATA,sha256=mlrOME9VDQLPlCg8odOOc2u_q261s4Ox5C71-SjU8R0,17433
43
- golem_vm_provider-0.1.53.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
44
- golem_vm_provider-0.1.53.dist-info/entry_points.txt,sha256=5Jiie1dIXygmxmDW66bKKxQpmBLJ7leSKRrb8bkQALw,52
45
- golem_vm_provider-0.1.53.dist-info/RECORD,,
42
+ golem_vm_provider-0.1.55.dist-info/METADATA,sha256=_GZ2hyX-aeTtK--VOiSE4tZfAnQwANw21tO63EvOskY,18877
43
+ golem_vm_provider-0.1.55.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
44
+ golem_vm_provider-0.1.55.dist-info/entry_points.txt,sha256=5Jiie1dIXygmxmDW66bKKxQpmBLJ7leSKRrb8bkQALw,52
45
+ golem_vm_provider-0.1.55.dist-info/RECORD,,
provider/config.py CHANGED
@@ -57,6 +57,17 @@ class Settings(BaseSettings):
57
57
  # Logical network selector for annotation and client defaults
58
58
  NETWORK: str = "mainnet" # one of: "testnet", "mainnet"
59
59
 
60
+ # Payments chain selection (modular network profiles). Keep default on l2.holesky
61
+ PAYMENTS_NETWORK: str = Field(
62
+ default="l2.holesky",
63
+ description="Payments network profile (e.g., 'l2.holesky', 'kaolin.holesky', 'mainnet')"
64
+ )
65
+
66
+ @field_validator("PAYMENTS_NETWORK", mode='before')
67
+ @classmethod
68
+ def prefer_payments_network_env(cls, v: str) -> str:
69
+ return os.environ.get("GOLEM_PROVIDER_PAYMENTS_NETWORK", v)
70
+
60
71
  @property
61
72
  def DEV_MODE(self) -> bool:
62
73
  return self.ENVIRONMENT == "development"
@@ -138,8 +149,8 @@ class Settings(BaseSettings):
138
149
 
139
150
  # Polygon / Payments
140
151
  POLYGON_RPC_URL: str = Field(
141
- default="https://l2.holesky.golemdb.io/rpc",
142
- description="EVM RPC URL for streaming payments (L2 by default)"
152
+ default="",
153
+ description="EVM RPC URL for streaming payments; defaults from PAYMENTS_NETWORK profile"
143
154
  )
144
155
  STREAM_PAYMENT_ADDRESS: str = Field(
145
156
  default="",
@@ -181,8 +192,8 @@ class Settings(BaseSettings):
181
192
 
182
193
  # L2 payments faucet (native ETH)
183
194
  L2_FAUCET_URL: str = Field(
184
- default="https://l2.holesky.golemdb.io/faucet",
185
- description="L2 faucet base URL (no trailing /api)"
195
+ default="",
196
+ description="Faucet base URL (no trailing /api). Only used on testnets; defaults from PAYMENTS_NETWORK profile"
186
197
  )
187
198
  L2_CAPTCHA_URL: str = Field(
188
199
  default="https://cap.gobas.me",
@@ -193,17 +204,42 @@ class Settings(BaseSettings):
193
204
  description="CAPTCHA API key path segment"
194
205
  )
195
206
 
207
+ @field_validator("L2_CAPTCHA_URL", mode='before')
208
+ @classmethod
209
+ def prefer_l2_captcha_url(cls, v: str) -> str:
210
+ return os.environ.get("GOLEM_PROVIDER_L2_CAPTCHA_URL", v)
211
+
212
+ @field_validator("L2_CAPTCHA_API_KEY", mode='before')
213
+ @classmethod
214
+ def prefer_l2_captcha_key(cls, v: str) -> str:
215
+ return os.environ.get("GOLEM_PROVIDER_L2_CAPTCHA_API_KEY", v)
216
+
196
217
  @field_validator("POLYGON_RPC_URL", mode='before')
197
218
  @classmethod
198
- def prefer_custom_env(cls, v: str) -> str:
219
+ def prefer_custom_env(cls, v: str, values: dict) -> str:
199
220
  # Accept alternative aliases for payments RPC
200
221
  for key in ("GOLEM_PROVIDER_L2_RPC_URL", "GOLEM_PROVIDER_KAOLIN_RPC_URL"):
201
222
  if os.environ.get(key):
202
223
  return os.environ[key]
203
- return v
224
+ if v:
225
+ return v
226
+ # Default from profile
227
+ pn = values.data.get("PAYMENTS_NETWORK") or "l2.holesky"
228
+ return Settings._profile_defaults(pn)["rpc_url"]
229
+
230
+ @field_validator("L2_FAUCET_URL", mode='before')
231
+ @classmethod
232
+ def prefer_faucet_env(cls, v: str, values: dict) -> str:
233
+ for key in ("GOLEM_PROVIDER_L2_FAUCET_URL",):
234
+ if os.environ.get(key):
235
+ return os.environ[key]
236
+ if v:
237
+ return v
238
+ pn = values.data.get("PAYMENTS_NETWORK") or "l2.holesky"
239
+ return Settings._profile_defaults(pn).get("faucet_url", "")
204
240
 
205
241
  @staticmethod
206
- def _load_l2_deployment() -> tuple[str | None, str | None]:
242
+ def _load_deployment(network: str) -> tuple[str | None, str | None]:
207
243
  """Try to load default StreamPayment + token from contracts/deployments/l2.json.
208
244
 
209
245
  Returns (stream_payment_address, glm_token_address) or (None, None) if not found.
@@ -212,15 +248,15 @@ class Settings(BaseSettings):
212
248
  # Allow override via env
213
249
  base = os.environ.get("GOLEM_DEPLOYMENTS_DIR")
214
250
  if base:
215
- path = Path(base) / "l2.json"
251
+ path = Path(base) / f"{Settings._deployment_basename(network)}.json"
216
252
  else:
217
253
  # repo root = ../../ from this file
218
- path = Path(__file__).resolve().parents[2] / "contracts" / "deployments" / "l2.json"
254
+ path = Path(__file__).resolve().parents[2] / "contracts" / "deployments" / f"{Settings._deployment_basename(network)}.json"
219
255
  if not path.exists():
220
256
  # Try package resource fallback
221
257
  try:
222
258
  import importlib.resources as ir
223
- with ir.files("provider.data.deployments").joinpath("l2.json").open("r") as fh: # type: ignore[attr-defined]
259
+ with ir.files("provider.data.deployments").joinpath(f"{Settings._deployment_basename(network)}.json").open("r") as fh: # type: ignore[attr-defined]
224
260
  data = json.load(fh)
225
261
  except Exception:
226
262
  return None, None
@@ -235,29 +271,90 @@ class Settings(BaseSettings):
235
271
  pass
236
272
  return None, None
237
273
 
274
+ # Backwards-compat helper used by tests expecting this method name
275
+ @staticmethod
276
+ def _load_l2_deployment() -> tuple[str | None, str | None]:
277
+ return Settings._load_deployment("l2.holesky")
278
+
279
+ @staticmethod
280
+ def _deployment_basename(network: str) -> str:
281
+ n = (network or "").lower()
282
+ if n in ("l2", "l2.holesky"):
283
+ return "l2"
284
+ if "." in n:
285
+ return n.split(".")[0]
286
+ return n or "l2"
287
+
288
+ @staticmethod
289
+ def _profile_defaults(network: str) -> dict[str, str | bool]:
290
+ n = (network or "l2.holesky").lower()
291
+ profiles = {
292
+ "l2.holesky": {
293
+ "rpc_url": "https://l2.holesky.golemdb.io/rpc",
294
+ "faucet_url": "https://l2.holesky.golemdb.io/faucet",
295
+ "faucet_enabled": True,
296
+ "token_symbol": "GLM",
297
+ "gas_symbol": "ETH",
298
+ },
299
+ "mainnet": {
300
+ "rpc_url": "",
301
+ "faucet_url": "",
302
+ "faucet_enabled": False,
303
+ "token_symbol": "GLM",
304
+ "gas_symbol": "ETH",
305
+ },
306
+ }
307
+ return profiles.get(n, profiles["l2.holesky"]) # default to current standard
308
+
238
309
  @field_validator("STREAM_PAYMENT_ADDRESS", mode='before')
239
310
  @classmethod
240
- def default_stream_addr(cls, v: str) -> str:
311
+ def default_stream_addr(cls, v: str, values: dict) -> str:
241
312
  # Disable payments during pytest to keep unit tests independent
242
313
  if os.environ.get("PYTEST_CURRENT_TEST"):
243
314
  return "0x0000000000000000000000000000000000000000"
244
315
  if v:
245
316
  return v
246
- addr, _ = Settings._load_l2_deployment()
317
+ pn = values.data.get("PAYMENTS_NETWORK") or "l2.holesky"
318
+ addr, _ = Settings._load_deployment(pn)
247
319
  return addr or "0x0000000000000000000000000000000000000000"
248
320
 
249
321
  @field_validator("GLM_TOKEN_ADDRESS", mode='before')
250
322
  @classmethod
251
- def default_token_addr(cls, v: str) -> str:
323
+ def default_token_addr(cls, v: str, values: dict) -> str:
252
324
  if os.environ.get("PYTEST_CURRENT_TEST"):
253
325
  return "0x0000000000000000000000000000000000000000"
254
326
  if v:
255
327
  return v
256
- _, token = Settings._load_l2_deployment()
328
+ pn = values.data.get("PAYMENTS_NETWORK") or "l2.holesky"
329
+ _, token = Settings._load_deployment(pn)
257
330
  return token or "0x0000000000000000000000000000000000000000"
258
331
 
259
332
  # VM Settings
260
333
  MAX_VMS: int = 10
334
+
335
+ # Optional human-friendly symbols from profile
336
+ TOKEN_SYMBOL: str = Field(default="", description="Payment token symbol, e.g., GLM")
337
+ GAS_TOKEN_SYMBOL: str = Field(default="", description="Gas token symbol, e.g., ETH")
338
+
339
+ @field_validator("TOKEN_SYMBOL", mode='before')
340
+ @classmethod
341
+ def default_token_symbol(cls, v: str, values: dict) -> str:
342
+ if v:
343
+ return v
344
+ pn = values.data.get("PAYMENTS_NETWORK") or "l2.holesky"
345
+ return str(Settings._profile_defaults(pn).get("token_symbol", ""))
346
+
347
+ @field_validator("GAS_TOKEN_SYMBOL", mode='before')
348
+ @classmethod
349
+ def default_gas_symbol(cls, v: str, values: dict) -> str:
350
+ if v:
351
+ return v
352
+ pn = values.data.get("PAYMENTS_NETWORK") or "l2.holesky"
353
+ return str(Settings._profile_defaults(pn).get("gas_symbol", ""))
354
+
355
+ @property
356
+ def FAUCET_ENABLED(self) -> bool:
357
+ return bool(self._profile_defaults(self.PAYMENTS_NETWORK).get("faucet_enabled", False))
261
358
  DEFAULT_VM_IMAGE: str = "ubuntu:24.04"
262
359
  VM_DATA_DIR: str = ""
263
360
  SSH_KEY_DIR: str = ""
@@ -68,6 +68,7 @@ class GolemBaseAdvertiser(Advertiser):
68
68
  string_annotations = [
69
69
  Annotation(key="golem_type", value="provider"),
70
70
  Annotation(key="golem_network", value=settings.NETWORK),
71
+ Annotation(key="golem_payments_network", value=settings.PAYMENTS_NETWORK),
71
72
  Annotation(key="golem_provider_id", value=settings.PROVIDER_ID),
72
73
  Annotation(key="golem_ip_address", value=ip_address),
73
74
  Annotation(key="golem_country", value=settings.PROVIDER_COUNTRY),
provider/main.py CHANGED
@@ -149,6 +149,9 @@ def wallet_faucet_l2():
149
149
  from .config import settings
150
150
  from .security.l2_faucet import L2FaucetService
151
151
  try:
152
+ if not bool(getattr(settings, "FAUCET_ENABLED", False)):
153
+ print("Faucet is disabled for current payments network.")
154
+ raise typer.Exit(code=0)
152
155
  addr = settings.PROVIDER_ID
153
156
  async def _run():
154
157
  svc = L2FaucetService(settings)
@@ -56,21 +56,41 @@ class StreamMonitor:
56
56
  logger.warning(f"stream {stream_id} lookup failed: {e}")
57
57
  continue
58
58
  # Stop VM if remaining runway < threshold
59
- remaining = max(s["stopTime"] - now, 0)
59
+ remaining = max(int(s["stopTime"]) - int(now), 0)
60
60
  logger.debug(
61
61
  f"stream {stream_id} for VM {vm_id}: start={s['startTime']} stop={s['stopTime']} "
62
62
  f"rate={s['ratePerSecond']} withdrawn={s['withdrawn']} halted={s['halted']} remaining={remaining}s"
63
63
  )
64
- if self._get("STREAM_MONITOR_ENABLED", False) and remaining < int(self._get("STREAM_MIN_REMAINING_SECONDS", 0)):
65
- logger.info(f"Stopping VM {vm_id} due to low stream runway ({remaining}s)")
64
+ # If stream is force-halted, delete immediately to free all resources
65
+ if bool(s.get("halted")):
66
+ logger.info(
67
+ f"Deleting VM {vm_id} due to halted stream (id={stream_id}, now={now})"
68
+ )
69
+ try:
70
+ await self.vm_service.delete_vm(vm_id)
71
+ except Exception as e:
72
+ logger.warning(f"delete_vm failed for {vm_id}: {e}")
73
+ try:
74
+ await self.stream_map.remove(vm_id)
75
+ except Exception as e:
76
+ logger.debug(f"failed to remove vm {vm_id} from stream map: {e}")
77
+ continue
78
+
79
+ # Only stop a VM when runway is completely empty
80
+ if remaining == 0:
81
+ logger.info(
82
+ f"Stopping VM {vm_id} as stream runway is exhausted (id={stream_id}, now={now}, stop={s.get('stopTime')})"
83
+ )
66
84
  try:
67
85
  await self.vm_service.stop_vm(vm_id)
68
86
  except Exception as e:
69
87
  logger.warning(f"stop_vm failed for {vm_id}: {e}")
70
- else:
71
- logger.debug(
72
- f"VM {vm_id} stream {stream_id} healthy (remaining={remaining}s, threshold={self._get('STREAM_MIN_REMAINING_SECONDS', 0)}s)"
73
- )
88
+ continue
89
+
90
+ # Otherwise, do not stop; just log health and consider withdrawals
91
+ logger.debug(
92
+ f"VM {vm_id} stream {stream_id} healthy (remaining={remaining}s)"
93
+ )
74
94
  # Withdraw if enough vested and configured
75
95
  if self._get("STREAM_WITHDRAW_ENABLED", False) and self.client:
76
96
  vested = max(min(now, s["stopTime"]) - s["startTime"], 0) * s["ratePerSecond"]
@@ -38,6 +38,10 @@ class L2FaucetService:
38
38
 
39
39
  Returns tx hash string on payout, or None if skipped/failed.
40
40
  """
41
+ # Respect profile gating only if explicitly present and false
42
+ if hasattr(self.cfg, "FAUCET_ENABLED") and not bool(getattr(self.cfg, "FAUCET_ENABLED")):
43
+ logger.info("Faucet disabled for current payments network; skipping.")
44
+ return None
41
45
  bal = self._balance_eth(address)
42
46
  if bal > 0.01:
43
47
  logger.info(f"Sufficient L2 funds ({bal} ETH), skipping faucet.")
@@ -60,4 +64,3 @@ class L2FaucetService:
60
64
  if tx:
61
65
  logger.success(f"L2 faucet sent tx: {tx}")
62
66
  return tx
63
-
provider/service.py CHANGED
@@ -37,6 +37,68 @@ class ProviderService:
37
37
  # Initialize services
38
38
  await self.port_manager.initialize()
39
39
  await self.vm_service.provider.initialize()
40
+
41
+ # Before starting advertisement, sync allocated resources with existing VMs
42
+ try:
43
+ vm_resources = await self.vm_service.get_all_vms_resources()
44
+ await self.vm_service.resource_tracker.sync_with_multipass(vm_resources)
45
+ except Exception as e:
46
+ logger.warning(f"Failed to sync resources with existing VMs: {e}")
47
+
48
+ # Cross-check running VMs against payment streams. If a VM has no
49
+ # active stream, it is no longer rented: terminate it and free resources.
50
+ try:
51
+ # Only perform checks if payments are configured
52
+ if settings.STREAM_PAYMENT_ADDRESS and not settings.STREAM_PAYMENT_ADDRESS.lower().endswith("0000000000000000000000000000000000000000") and settings.POLYGON_RPC_URL:
53
+ stream_map = app.container.stream_map()
54
+ reader = app.container.stream_reader()
55
+
56
+ # Use the most recent view of VMs from the previous sync
57
+ vm_ids = list(vm_resources.keys()) if 'vm_resources' in locals() else []
58
+ for vm_id in vm_ids:
59
+ try:
60
+ stream_id = await stream_map.get(vm_id)
61
+ except Exception:
62
+ stream_id = None
63
+
64
+ if stream_id is None:
65
+ reason = "no stream mapped"
66
+ should_terminate = True
67
+ else:
68
+ try:
69
+ ok, msg = reader.verify_stream(int(stream_id), settings.PROVIDER_ID)
70
+ should_terminate = not ok
71
+ reason = msg if not ok else "ok"
72
+ except Exception as e:
73
+ # If verification cannot be performed, be conservative and keep the VM
74
+ logger.warning(f"Stream verification error for VM {vm_id} (stream {stream_id}): {e}")
75
+ should_terminate = False
76
+ reason = f"verification error: {e}"
77
+
78
+ if should_terminate:
79
+ logger.info(
80
+ f"Deleting VM {vm_id}: inactive stream (stream_id={stream_id}, reason={reason})"
81
+ )
82
+ try:
83
+ await self.vm_service.delete_vm(vm_id)
84
+ except Exception as e:
85
+ logger.warning(f"Failed to delete VM {vm_id}: {e}")
86
+ try:
87
+ await stream_map.remove(vm_id)
88
+ except Exception:
89
+ pass
90
+
91
+ # Re-sync after any terminations to ensure ads reflect capacity
92
+ try:
93
+ vm_resources = await self.vm_service.get_all_vms_resources()
94
+ await self.vm_service.resource_tracker.sync_with_multipass(vm_resources)
95
+ except Exception as e:
96
+ logger.warning(f"Post-termination resource sync failed: {e}")
97
+ else:
98
+ logger.info("Payments not configured; skipping startup stream checks")
99
+ except Exception as e:
100
+ logger.warning(f"Failed to reconcile VMs with payment streams: {e}")
101
+
40
102
  await self.advertisement_service.start()
41
103
  # Start pricing auto-updater; trigger re-advertise after updates
42
104
  async def _on_price_updated(platform: str, glm_usd):