golem-vm-provider 0.1.52__py3-none-any.whl → 0.1.54__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.52
3
+ Version: 0.1.54
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
 
@@ -454,14 +488,16 @@ poetry run golem-provider streams withdraw --vm-id <vm_id>
454
488
  poetry run golem-provider streams withdraw --all
455
489
  ```
456
490
 
491
+ Note: On testnets, the withdraw command auto-attempts to fund the provider's L2 address via the configured faucet if native gas balance is low.
492
+
457
493
  Configure monitor and withdraw via CLI:
458
494
 
459
495
  ```bash
460
496
  # Set monitor to require 1h remaining, check every 30s
461
- 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
462
498
 
463
499
  # Enable auto-withdraw every 15 minutes when >= 1e15 wei
464
- 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
465
501
  ```
466
502
 
467
503
  ### Resource Advertisement Flow
@@ -2,24 +2,25 @@ 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=-dkQEwMrU2g1ljGrdf2B6P6CcgpDAjeCEqbBy_E4ybk,32799
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
19
  provider/payments/monitor.py,sha256=Rw17zYsxZre0zU6R0oeRNvVIzMdXLsgoUvSPHpJy6I0,4488
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=yRV4xdPBgU8-LDTLqtuAijfgIoe2kYxvXqJLxFd-BVI,2662
23
24
  provider/service.py,sha256=IIjeSM9T4r616nBRnxCUum_sgbyRusMMcja3yQd8zQI,3383
24
25
  provider/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
25
26
  provider/utils/ascii_art.py,sha256=ykBFsztk57GIiz1NJ-EII5UvN74iECqQL4h9VmiW6Z8,3161
@@ -38,7 +39,7 @@ provider/vm/port_manager.py,sha256=iYSwjTjD_ziOhG8aI7juKHw1OwwRUTJQyQoRUNQvz9w,1
38
39
  provider/vm/provider.py,sha256=A7QN89EJjcSS40_SmKeinG1Jp_NGffJaLse-XdKciAs,1164
39
40
  provider/vm/proxy_manager.py,sha256=n4NTsyz2rtrvjtf_ceKBk-g2q_mzqPwruB1q7UlQVBc,14928
40
41
  provider/vm/service.py,sha256=Ki4SGNIZUq3XmaPMwAOoNzdZzKQsmFXid374wgjFPes,4636
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,,
42
+ golem_vm_provider-0.1.54.dist-info/METADATA,sha256=u7WDC-uHV2F5NJ1ayjL7Vlz2s1yXDRFrOIolk_7V1EM,18452
43
+ golem_vm_provider-0.1.54.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
44
+ golem_vm_provider-0.1.54.dist-info/entry_points.txt,sha256=5Jiie1dIXygmxmDW66bKKxQpmBLJ7leSKRrb8bkQALw,52
45
+ golem_vm_provider-0.1.54.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
@@ -147,38 +147,20 @@ def main():
147
147
  def wallet_faucet_l2():
148
148
  """Request L2 faucet funds for the provider's payment address (native ETH)."""
149
149
  from .config import settings
150
- from golem_faucet import PowFaucetClient
151
- from web3 import Web3
150
+ from .security.l2_faucet import L2FaucetService
152
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)
153
155
  addr = settings.PROVIDER_ID
154
- faucet = PowFaucetClient(settings.L2_FAUCET_URL, settings.L2_CAPTCHA_URL, settings.L2_CAPTCHA_API_KEY)
155
- # Check current L2 balance
156
- w3 = Web3(Web3.HTTPProvider(settings.POLYGON_RPC_URL))
157
- bal = 0.0
158
- try:
159
- bal = float(w3.from_wei(w3.eth.get_balance(Web3.to_checksum_address(addr)), 'ether'))
160
- except Exception:
161
- pass
162
- if bal > 0.01:
163
- print(f"Sufficient L2 funds ({bal} ETH); skipping faucet.")
164
- return
165
156
  async def _run():
166
- chall = await faucet.get_challenge()
167
- if not chall:
168
- print("Failed to get challenge")
169
- raise typer.Exit(code=1)
170
- sols = []
171
- for salt, target in chall.get('challenge') or []:
172
- sols.append((salt, target, PowFaucetClient.solve_challenge(salt, target)))
173
- redeemed = await faucet.redeem(chall.get('token'), sols)
174
- if not redeemed:
175
- print("Failed to redeem solutions")
176
- raise typer.Exit(code=1)
177
- tx = await faucet.request_funds(addr, redeemed)
157
+ svc = L2FaucetService(settings)
158
+ tx = await svc.request_funds(addr)
178
159
  if tx:
179
160
  print(f"Faucet tx: {tx}")
180
161
  else:
181
- print("Faucet request failed")
162
+ # Either skipped due to sufficient balance or failed
163
+ pass
182
164
  asyncio.run(_run())
183
165
  except Exception as e:
184
166
  print(f"Error: {e}")
@@ -497,6 +479,7 @@ def streams_withdraw(
497
479
  """Withdraw vested funds for one or all streams."""
498
480
  from .container import Container
499
481
  from .config import settings
482
+ from .security.l2_faucet import L2FaucetService
500
483
  try:
501
484
  if not vm_id and not all_streams:
502
485
  print("Specify --vm-id or --all")
@@ -505,6 +488,12 @@ def streams_withdraw(
505
488
  c.config.from_pydantic(settings)
506
489
  stream_map = c.stream_map()
507
490
  client = c.stream_client()
491
+ # Ensure we have L2 gas for withdrawals (testnets)
492
+ try:
493
+ asyncio.run(L2FaucetService(settings).request_funds(settings.PROVIDER_ID))
494
+ except Exception:
495
+ # Non-fatal; proceed with withdraw attempt
496
+ pass
508
497
  targets = []
509
498
  if all_streams:
510
499
  items = asyncio.run(stream_map.all_items())
@@ -0,0 +1,66 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Optional, List, Tuple
4
+
5
+ from web3 import Web3
6
+ from golem_faucet import PowFaucetClient
7
+ from ..utils.logging import setup_logger
8
+
9
+ logger = setup_logger(__name__)
10
+
11
+
12
+ class L2FaucetService:
13
+ """Request native ETH on the L2 payments chain via PoW faucet.
14
+
15
+ Uses provider settings for RPC and faucet endpoints.
16
+ """
17
+
18
+ def __init__(self, config):
19
+ # Config is expected to expose POLYGON_RPC_URL, L2_* faucet fields
20
+ self.cfg = config
21
+ self.web3 = Web3(Web3.HTTPProvider(config.POLYGON_RPC_URL))
22
+ self.client = PowFaucetClient(
23
+ faucet_url=getattr(config, "L2_FAUCET_URL", "https://l2.holesky.golemdb.io/faucet"),
24
+ captcha_base_url=getattr(config, "L2_CAPTCHA_URL", "https://cap.gobas.me"),
25
+ captcha_api_key=getattr(config, "L2_CAPTCHA_API_KEY", "05381a2cef5e"),
26
+ )
27
+
28
+ def _balance_eth(self, address: str) -> float:
29
+ try:
30
+ wei = self.web3.eth.get_balance(Web3.to_checksum_address(address))
31
+ return float(self.web3.from_wei(wei, "ether"))
32
+ except Exception as e:
33
+ logger.warning(f"L2 balance check failed: {e}")
34
+ return 0.0
35
+
36
+ async def request_funds(self, address: str) -> Optional[str]:
37
+ """Ensure some native ETH on L2; if low, solve PoW and request faucet payout.
38
+
39
+ Returns tx hash string on payout, or None if skipped/failed.
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
45
+ bal = self._balance_eth(address)
46
+ if bal > 0.01:
47
+ logger.info(f"Sufficient L2 funds ({bal} ETH), skipping faucet.")
48
+ return None
49
+ chall = await self.client.get_challenge()
50
+ if not chall:
51
+ logger.error("could not fetch faucet challenge")
52
+ return None
53
+ token = chall.get("token")
54
+ challenge_list = chall.get("challenge") or []
55
+ solutions: List[Tuple[str, str, int]] = []
56
+ for salt, target in challenge_list:
57
+ nonce = PowFaucetClient.solve_challenge(salt, target)
58
+ solutions.append((salt, target, nonce))
59
+ redeemed = await self.client.redeem(token, solutions)
60
+ if not redeemed:
61
+ logger.error("failed to redeem challenge")
62
+ return None
63
+ tx = await self.client.request_funds(address, redeemed)
64
+ if tx:
65
+ logger.success(f"L2 faucet sent tx: {tx}")
66
+ return tx