golem-vm-provider 0.1.39__tar.gz → 0.1.42__tar.gz

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.
Files changed (42) hide show
  1. {golem_vm_provider-0.1.39 → golem_vm_provider-0.1.42}/PKG-INFO +51 -8
  2. {golem_vm_provider-0.1.39 → golem_vm_provider-0.1.42}/README.md +48 -7
  3. {golem_vm_provider-0.1.39 → golem_vm_provider-0.1.42}/provider/api/routes.py +12 -0
  4. {golem_vm_provider-0.1.39 → golem_vm_provider-0.1.42}/provider/config.py +34 -6
  5. {golem_vm_provider-0.1.39 → golem_vm_provider-0.1.42}/provider/discovery/golem_base_advertiser.py +1 -0
  6. {golem_vm_provider-0.1.39 → golem_vm_provider-0.1.42}/provider/main.py +68 -9
  7. {golem_vm_provider-0.1.39 → golem_vm_provider-0.1.42}/provider/payments/blockchain_service.py +7 -49
  8. {golem_vm_provider-0.1.39 → golem_vm_provider-0.1.42}/provider/payments/monitor.py +5 -2
  9. {golem_vm_provider-0.1.39 → golem_vm_provider-0.1.42}/provider/security/faucet.py +17 -41
  10. {golem_vm_provider-0.1.39 → golem_vm_provider-0.1.42}/pyproject.toml +5 -4
  11. {golem_vm_provider-0.1.39 → golem_vm_provider-0.1.42}/provider/__init__.py +0 -0
  12. {golem_vm_provider-0.1.39 → golem_vm_provider-0.1.42}/provider/api/__init__.py +0 -0
  13. {golem_vm_provider-0.1.39 → golem_vm_provider-0.1.42}/provider/api/models.py +0 -0
  14. {golem_vm_provider-0.1.39 → golem_vm_provider-0.1.42}/provider/container.py +0 -0
  15. {golem_vm_provider-0.1.39 → golem_vm_provider-0.1.42}/provider/discovery/__init__.py +0 -0
  16. {golem_vm_provider-0.1.39 → golem_vm_provider-0.1.42}/provider/discovery/advertiser.py +0 -0
  17. {golem_vm_provider-0.1.39 → golem_vm_provider-0.1.42}/provider/discovery/golem_base_utils.py +0 -0
  18. {golem_vm_provider-0.1.39 → golem_vm_provider-0.1.42}/provider/discovery/multi_advertiser.py +0 -0
  19. {golem_vm_provider-0.1.39 → golem_vm_provider-0.1.42}/provider/discovery/resource_monitor.py +0 -0
  20. {golem_vm_provider-0.1.39 → golem_vm_provider-0.1.42}/provider/discovery/resource_tracker.py +0 -0
  21. {golem_vm_provider-0.1.39 → golem_vm_provider-0.1.42}/provider/discovery/service.py +0 -0
  22. {golem_vm_provider-0.1.39 → golem_vm_provider-0.1.42}/provider/network/port_verifier.py +0 -0
  23. {golem_vm_provider-0.1.39 → golem_vm_provider-0.1.42}/provider/payments/stream_map.py +0 -0
  24. {golem_vm_provider-0.1.39 → golem_vm_provider-0.1.42}/provider/security/ethereum.py +0 -0
  25. {golem_vm_provider-0.1.39 → golem_vm_provider-0.1.42}/provider/service.py +0 -0
  26. {golem_vm_provider-0.1.39 → golem_vm_provider-0.1.42}/provider/utils/__init__.py +0 -0
  27. {golem_vm_provider-0.1.39 → golem_vm_provider-0.1.42}/provider/utils/ascii_art.py +0 -0
  28. {golem_vm_provider-0.1.39 → golem_vm_provider-0.1.42}/provider/utils/logging.py +0 -0
  29. {golem_vm_provider-0.1.39 → golem_vm_provider-0.1.42}/provider/utils/port_display.py +0 -0
  30. {golem_vm_provider-0.1.39 → golem_vm_provider-0.1.42}/provider/utils/pricing.py +0 -0
  31. {golem_vm_provider-0.1.39 → golem_vm_provider-0.1.42}/provider/utils/retry.py +0 -0
  32. {golem_vm_provider-0.1.39 → golem_vm_provider-0.1.42}/provider/utils/setup.py +0 -0
  33. {golem_vm_provider-0.1.39 → golem_vm_provider-0.1.42}/provider/vm/__init__.py +0 -0
  34. {golem_vm_provider-0.1.39 → golem_vm_provider-0.1.42}/provider/vm/cloud_init.py +0 -0
  35. {golem_vm_provider-0.1.39 → golem_vm_provider-0.1.42}/provider/vm/models.py +0 -0
  36. {golem_vm_provider-0.1.39 → golem_vm_provider-0.1.42}/provider/vm/multipass.py +0 -0
  37. {golem_vm_provider-0.1.39 → golem_vm_provider-0.1.42}/provider/vm/multipass_adapter.py +0 -0
  38. {golem_vm_provider-0.1.39 → golem_vm_provider-0.1.42}/provider/vm/name_mapper.py +0 -0
  39. {golem_vm_provider-0.1.39 → golem_vm_provider-0.1.42}/provider/vm/port_manager.py +0 -0
  40. {golem_vm_provider-0.1.39 → golem_vm_provider-0.1.42}/provider/vm/provider.py +0 -0
  41. {golem_vm_provider-0.1.39 → golem_vm_provider-0.1.42}/provider/vm/proxy_manager.py +0 -0
  42. {golem_vm_provider-0.1.39 → golem_vm_provider-0.1.42}/provider/vm/service.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: golem-vm-provider
3
- Version: 0.1.39
3
+ Version: 0.1.42
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
@@ -23,6 +23,8 @@ Requires-Dist: dependency-injector (>=4.41.0,<5.0.0)
23
23
  Requires-Dist: eth-account (>=0.13.6,<0.14.0)
24
24
  Requires-Dist: fastapi (>=0.103.0,<0.104.0)
25
25
  Requires-Dist: golem-base-sdk (==0.1.0)
26
+ Requires-Dist: golem-faucet (>=0.1.0,<0.2.0)
27
+ Requires-Dist: golem-streaming-abi (>=0.1.0,<0.2.0)
26
28
  Requires-Dist: httpx (>=0.23.0,<0.24.0)
27
29
  Requires-Dist: psutil (>=5.9.0,<6.0.0)
28
30
  Requires-Dist: pydantic (>=2.4.0,<3.0.0)
@@ -228,15 +230,19 @@ GOLEM_PROVIDER_PUBLIC_IP="auto"
228
230
  # Discovery Settings
229
231
  GOLEM_PROVIDER_DISCOVERY_URL="http://discovery.golem.network:9001"
230
232
  GOLEM_PROVIDER_ADVERTISEMENT_INTERVAL=240
233
+
234
+ # Network Selection
235
+ # Adds an annotation to on-chain advertisements and can be used by requestors to filter
236
+ GOLEM_PROVIDER_NETWORK="testnet" # or "mainnet"
231
237
  ```
232
238
 
233
- ### Streaming Payments (Polygon GLM)
239
+ ### Streaming Payments (Native ETH on L2)
234
240
 
235
- Enable on‑chain stream‑gated rentals by configuring the following (env prefix `GOLEM_PROVIDER_`):
241
+ Enable on‑chain stream‑gated rentals funded in native ETH. Configure (env prefix `GOLEM_PROVIDER_`):
236
242
 
237
- - `POLYGON_RPC_URL` — Polygon PoS RPC URL (e.g., https://polygon-rpc.com)
243
+ - `POLYGON_RPC_URL` — EVM RPC URL (default points to L2: https://l2.holesky.golemdb.io/rpc)
238
244
  - `STREAM_PAYMENT_ADDRESS` — StreamPayment contract address; if non‑zero, VM creation requires a valid `stream_id`
239
- - `GLM_TOKEN_ADDRESS` — GLM ERC20 address (for info endpoint)
245
+ - `GLM_TOKEN_ADDRESS` — Token address; set to `0x0000000000000000000000000000000000000000` to indicate native ETH
240
246
 
241
247
  Optional background automation (all disabled by default):
242
248
 
@@ -247,6 +253,12 @@ Optional background automation (all disabled by default):
247
253
  - `STREAM_WITHDRAW_INTERVAL_SECONDS` — how often to attempt withdrawals (default 1800)
248
254
  - `STREAM_MIN_WITHDRAW_WEI` — only withdraw when >= this amount (gas‑aware)
249
255
 
256
+ Implementation notes:
257
+
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
+ - 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
+ - When a VM is deleted, the VM→stream mapping is cleaned up.
261
+
250
262
  When enabled, the provider verifies each VM creation request’s `stream_id` and refuses to start the VM if:
251
263
 
252
264
  - stream recipient != provider’s Ethereum address
@@ -310,7 +322,8 @@ Response:
310
322
  {
311
323
  "provider_id": "0xProviderEthereumAddress",
312
324
  "stream_payment_address": "0xStreamPayment",
313
- "glm_token_address": "0xGLM"
325
+ "glm_token_address": "0x0000000000000000000000000000000000000000"
326
+
314
327
  }
315
328
  ```
316
329
 
@@ -324,10 +337,26 @@ Use this endpoint to discover the correct recipient for creating a GLM stream.
324
337
  # To run in production mode
325
338
  poetry run golem-provider start
326
339
 
327
- # To run in development mode
328
- poetry run golem-provider dev
340
+ # To run in development mode, set the environment and optionally network
341
+ GOLEM_PROVIDER_ENVIRONMENT=development poetry run golem-provider start --network testnet
329
342
  ```
330
343
 
344
+ ### Mode vs. Network
345
+
346
+ - Development Mode (`GOLEM_PROVIDER_ENVIRONMENT=development`)
347
+ - Optimizes for local iteration: enables reload + debug logging and uses local defaults (e.g., local port check servers). May derive a local/LAN IP automatically and prefix the provider name with `DEVMODE-`.
348
+ - Does not decide which chain you target.
349
+
350
+ - Network Selection (`--network` or `GOLEM_PROVIDER_NETWORK`)
351
+ - Chooses the discovery/advertisement scope: providers advertise `golem_network=testnet|mainnet` and requestors filter accordingly.
352
+ - Pair with appropriate RPC envs (`GOLEM_PROVIDER_GOLEM_BASE_RPC_URL`, `GOLEM_PROVIDER_GOLEM_BASE_WS_URL`).
353
+ - Does not change dev ergonomics (logging, reload, or port verification behavior).
354
+
355
+ Common setups:
356
+ - Local dev on testnet: `GOLEM_PROVIDER_ENVIRONMENT=development` plus `--network testnet`.
357
+ - Staging on testnet: keep `ENVIRONMENT=production`, set `--network testnet` and testnet RPCs.
358
+ - Production on mainnet: `ENVIRONMENT=production` with `--network mainnet` and mainnet RPCs.
359
+
331
360
  The provider will:
332
361
 
333
362
  1. Verify port accessibility
@@ -339,6 +368,20 @@ The provider will:
339
368
  4. Begin resource advertisement
340
369
  5. Listen for VM requests
341
370
 
371
+ ### Faucet
372
+
373
+ - L3 (Golem Base adverts): provider auto-requests funds on startup from `FAUCET_URL` (defaults to EthWarsaw Holesky) protected by CAPTCHA at `CAPTCHA_URL/05381a2cef5e`.
374
+ - L2 (payments): Use the CLI to request native ETH:
375
+
376
+ ```bash
377
+ poetry run golem-provider wallet faucet-l2
378
+ ```
379
+
380
+ Defaults:
381
+ - L2 faucet: `https://l2.holesky.golemdb.io/faucet`
382
+ - CAPTCHA: `https://cap.gobas.me/05381a2cef5e`
383
+ - Override with env: `GOLEM_PROVIDER_L2_FAUCET_URL`, `GOLEM_PROVIDER_L2_CAPTCHA_URL`, `GOLEM_PROVIDER_L2_CAPTCHA_API_KEY`.
384
+
342
385
  ### Resource Advertisement Flow
343
386
 
344
387
  ```mermaid
@@ -185,15 +185,19 @@ GOLEM_PROVIDER_PUBLIC_IP="auto"
185
185
  # Discovery Settings
186
186
  GOLEM_PROVIDER_DISCOVERY_URL="http://discovery.golem.network:9001"
187
187
  GOLEM_PROVIDER_ADVERTISEMENT_INTERVAL=240
188
+
189
+ # Network Selection
190
+ # Adds an annotation to on-chain advertisements and can be used by requestors to filter
191
+ GOLEM_PROVIDER_NETWORK="testnet" # or "mainnet"
188
192
  ```
189
193
 
190
- ### Streaming Payments (Polygon GLM)
194
+ ### Streaming Payments (Native ETH on L2)
191
195
 
192
- Enable on‑chain stream‑gated rentals by configuring the following (env prefix `GOLEM_PROVIDER_`):
196
+ Enable on‑chain stream‑gated rentals funded in native ETH. Configure (env prefix `GOLEM_PROVIDER_`):
193
197
 
194
- - `POLYGON_RPC_URL` — Polygon PoS RPC URL (e.g., https://polygon-rpc.com)
198
+ - `POLYGON_RPC_URL` — EVM RPC URL (default points to L2: https://l2.holesky.golemdb.io/rpc)
195
199
  - `STREAM_PAYMENT_ADDRESS` — StreamPayment contract address; if non‑zero, VM creation requires a valid `stream_id`
196
- - `GLM_TOKEN_ADDRESS` — GLM ERC20 address (for info endpoint)
200
+ - `GLM_TOKEN_ADDRESS` — Token address; set to `0x0000000000000000000000000000000000000000` to indicate native ETH
197
201
 
198
202
  Optional background automation (all disabled by default):
199
203
 
@@ -204,6 +208,12 @@ Optional background automation (all disabled by default):
204
208
  - `STREAM_WITHDRAW_INTERVAL_SECONDS` — how often to attempt withdrawals (default 1800)
205
209
  - `STREAM_MIN_WITHDRAW_WEI` — only withdraw when >= this amount (gas‑aware)
206
210
 
211
+ Implementation notes:
212
+
213
+ - 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.
214
+ - 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.
215
+ - When a VM is deleted, the VM→stream mapping is cleaned up.
216
+
207
217
  When enabled, the provider verifies each VM creation request’s `stream_id` and refuses to start the VM if:
208
218
 
209
219
  - stream recipient != provider’s Ethereum address
@@ -267,7 +277,8 @@ Response:
267
277
  {
268
278
  "provider_id": "0xProviderEthereumAddress",
269
279
  "stream_payment_address": "0xStreamPayment",
270
- "glm_token_address": "0xGLM"
280
+ "glm_token_address": "0x0000000000000000000000000000000000000000"
281
+
271
282
  }
272
283
  ```
273
284
 
@@ -281,10 +292,26 @@ Use this endpoint to discover the correct recipient for creating a GLM stream.
281
292
  # To run in production mode
282
293
  poetry run golem-provider start
283
294
 
284
- # To run in development mode
285
- poetry run golem-provider dev
295
+ # To run in development mode, set the environment and optionally network
296
+ GOLEM_PROVIDER_ENVIRONMENT=development poetry run golem-provider start --network testnet
286
297
  ```
287
298
 
299
+ ### Mode vs. Network
300
+
301
+ - Development Mode (`GOLEM_PROVIDER_ENVIRONMENT=development`)
302
+ - Optimizes for local iteration: enables reload + debug logging and uses local defaults (e.g., local port check servers). May derive a local/LAN IP automatically and prefix the provider name with `DEVMODE-`.
303
+ - Does not decide which chain you target.
304
+
305
+ - Network Selection (`--network` or `GOLEM_PROVIDER_NETWORK`)
306
+ - Chooses the discovery/advertisement scope: providers advertise `golem_network=testnet|mainnet` and requestors filter accordingly.
307
+ - Pair with appropriate RPC envs (`GOLEM_PROVIDER_GOLEM_BASE_RPC_URL`, `GOLEM_PROVIDER_GOLEM_BASE_WS_URL`).
308
+ - Does not change dev ergonomics (logging, reload, or port verification behavior).
309
+
310
+ Common setups:
311
+ - Local dev on testnet: `GOLEM_PROVIDER_ENVIRONMENT=development` plus `--network testnet`.
312
+ - Staging on testnet: keep `ENVIRONMENT=production`, set `--network testnet` and testnet RPCs.
313
+ - Production on mainnet: `ENVIRONMENT=production` with `--network mainnet` and mainnet RPCs.
314
+
288
315
  The provider will:
289
316
 
290
317
  1. Verify port accessibility
@@ -296,6 +323,20 @@ The provider will:
296
323
  4. Begin resource advertisement
297
324
  5. Listen for VM requests
298
325
 
326
+ ### Faucet
327
+
328
+ - L3 (Golem Base adverts): provider auto-requests funds on startup from `FAUCET_URL` (defaults to EthWarsaw Holesky) protected by CAPTCHA at `CAPTCHA_URL/05381a2cef5e`.
329
+ - L2 (payments): Use the CLI to request native ETH:
330
+
331
+ ```bash
332
+ poetry run golem-provider wallet faucet-l2
333
+ ```
334
+
335
+ Defaults:
336
+ - L2 faucet: `https://l2.holesky.golemdb.io/faucet`
337
+ - CAPTCHA: `https://cap.gobas.me/05381a2cef5e`
338
+ - Override with env: `GOLEM_PROVIDER_L2_FAUCET_URL`, `GOLEM_PROVIDER_L2_CAPTCHA_URL`, `GOLEM_PROVIDER_L2_CAPTCHA_API_KEY`.
339
+
299
340
  ### Resource Advertisement Flow
300
341
 
301
342
  ```mermaid
@@ -26,6 +26,7 @@ async def create_vm(
26
26
  request: CreateVMRequest,
27
27
  vm_service: VMService = Depends(Provide[Container.vm_service]),
28
28
  settings: Settings = Depends(Provide[Container.config]),
29
+ stream_map = Depends(Provide[Container.stream_map]),
29
30
  ) -> VMInfo:
30
31
  """Create a new VM."""
31
32
  try:
@@ -51,6 +52,12 @@ async def create_vm(
51
52
  )
52
53
 
53
54
  vm_info = await vm_service.create_vm(config)
55
+ # Persist VM->stream mapping if provided
56
+ if request.stream_id is not None:
57
+ try:
58
+ await stream_map.set(vm_info.id, int(request.stream_id))
59
+ except Exception as e:
60
+ logger.warning(f"failed to persist stream mapping for {vm_info.id}: {e}")
54
61
  await vm_creation_animation(request.name)
55
62
  return vm_info
56
63
  except MultipassError as e:
@@ -164,12 +171,17 @@ async def stop_vm(
164
171
  async def delete_vm(
165
172
  requestor_name: str,
166
173
  vm_service: VMService = Depends(Provide[Container.vm_service]),
174
+ stream_map = Depends(Provide[Container.stream_map]),
167
175
  ) -> None:
168
176
  """Delete a VM."""
169
177
  try:
170
178
  logger.process(f"🗑️ Deleting VM '{requestor_name}'")
171
179
  vm_status_change(requestor_name, "STOPPING", "Cleanup in progress")
172
180
  await vm_service.delete_vm(requestor_name)
181
+ try:
182
+ await stream_map.remove(requestor_name)
183
+ except Exception as e:
184
+ logger.warning(f"failed to remove stream mapping for {requestor_name}: {e}")
173
185
  vm_status_change(requestor_name, "TERMINATED", "Cleanup complete")
174
186
  logger.success(f"✨ Successfully deleted VM '{requestor_name}'")
175
187
  except VMNotFoundError as e:
@@ -6,6 +6,7 @@ import socket
6
6
 
7
7
  from pydantic_settings import BaseSettings
8
8
  from pydantic import field_validator, Field
9
+ import os
9
10
  from .utils.logging import setup_logger
10
11
 
11
12
  logger = setup_logger(__name__)
@@ -52,6 +53,8 @@ class Settings(BaseSettings):
52
53
  PORT: int = 7466
53
54
  SKIP_PORT_VERIFICATION: bool = False
54
55
  ENVIRONMENT: str = "production"
56
+ # Logical network selector for annotation and client defaults
57
+ NETWORK: str = "mainnet" # one of: "testnet", "mainnet"
55
58
 
56
59
  @property
57
60
  def DEV_MODE(self) -> bool:
@@ -68,9 +71,6 @@ class Settings(BaseSettings):
68
71
  ETHEREUM_KEY_DIR: str = ""
69
72
  ETHEREUM_PRIVATE_KEY: Optional[str] = None
70
73
  PROVIDER_ID: str = "" # Will be set from Ethereum identity
71
- FAUCET_URL: str = "https://ethwarsaw.holesky.golemdb.io/faucet"
72
- CAPTCHA_URL: str = "https://cap.gobas.me"
73
- CAPTCHA_API_KEY: str = "05381a2cef5e"
74
74
 
75
75
  @field_validator("ETHEREUM_KEY_DIR", mode='before')
76
76
  def resolve_key_dir(cls, v: str) -> str:
@@ -137,8 +137,8 @@ class Settings(BaseSettings):
137
137
 
138
138
  # Polygon / Payments
139
139
  POLYGON_RPC_URL: str = Field(
140
- default="https://polygon-rpc.com",
141
- description="Polygon PoS RPC URL for GLM payments"
140
+ default="https://l2.holesky.golemdb.io/rpc",
141
+ description="EVM RPC URL for streaming payments (L2 by default)"
142
142
  )
143
143
  STREAM_PAYMENT_ADDRESS: str = Field(
144
144
  default="0x0000000000000000000000000000000000000000",
@@ -146,7 +146,7 @@ class Settings(BaseSettings):
146
146
  )
147
147
  GLM_TOKEN_ADDRESS: str = Field(
148
148
  default="0x0000000000000000000000000000000000000000",
149
- description="GLM ERC20 token address on target network"
149
+ description="Token address (0x0 means native ETH)"
150
150
  )
151
151
  STREAM_MIN_REMAINING_SECONDS: int = Field(
152
152
  default=3600,
@@ -173,6 +173,34 @@ class Settings(BaseSettings):
173
173
  description="Min withdrawable amount (wei) before triggering withdraw"
174
174
  )
175
175
 
176
+ # Faucet settings (L3 for Golem Base adverts)
177
+ FAUCET_URL: str = "https://ethwarsaw.holesky.golemdb.io/faucet"
178
+ CAPTCHA_URL: str = "https://cap.gobas.me"
179
+ CAPTCHA_API_KEY: str = "05381a2cef5e"
180
+
181
+ # L2 payments faucet (native ETH)
182
+ L2_FAUCET_URL: str = Field(
183
+ default="https://l2.holesky.golemdb.io/faucet",
184
+ description="L2 faucet base URL (no trailing /api)"
185
+ )
186
+ L2_CAPTCHA_URL: str = Field(
187
+ default="https://cap.gobas.me",
188
+ description="CAPTCHA base URL"
189
+ )
190
+ L2_CAPTCHA_API_KEY: str = Field(
191
+ default="05381a2cef5e",
192
+ description="CAPTCHA API key path segment"
193
+ )
194
+
195
+ @field_validator("POLYGON_RPC_URL", mode='before')
196
+ @classmethod
197
+ def prefer_custom_env(cls, v: str) -> str:
198
+ # Accept alternative aliases for payments RPC
199
+ for key in ("GOLEM_PROVIDER_L2_RPC_URL", "GOLEM_PROVIDER_KAOLIN_RPC_URL"):
200
+ if os.environ.get(key):
201
+ return os.environ[key]
202
+ return v
203
+
176
204
  # VM Settings
177
205
  MAX_VMS: int = 10
178
206
  DEFAULT_VM_IMAGE: str = "ubuntu:24.04"
@@ -66,6 +66,7 @@ class GolemBaseAdvertiser(Advertiser):
66
66
 
67
67
  string_annotations = [
68
68
  Annotation(key="golem_type", value="provider"),
69
+ Annotation(key="golem_network", value=settings.NETWORK),
69
70
  Annotation(key="golem_provider_id", value=settings.PROVIDER_ID),
70
71
  Annotation(key="golem_ip_address", value=ip_address),
71
72
  Annotation(key="golem_country", value=settings.PROVIDER_COUNTRY),
@@ -122,7 +122,9 @@ except ImportError:
122
122
 
123
123
  cli = typer.Typer()
124
124
  pricing_app = typer.Typer(help="Configure USD pricing; auto-converts to GLM.")
125
+ wallet_app = typer.Typer(help="Wallet utilities (funding, balance)")
125
126
  cli.add_typer(pricing_app, name="pricing")
127
+ cli.add_typer(wallet_app, name="wallet")
126
128
 
127
129
  def print_version(ctx: typer.Context, value: bool):
128
130
  if not value:
@@ -141,15 +143,57 @@ def main(
141
143
  ensure_config()
142
144
  pass
143
145
 
146
+
147
+ @wallet_app.command("faucet-l2")
148
+ def wallet_faucet_l2():
149
+ """Request L2 faucet funds for the provider's payment address (native ETH)."""
150
+ from .config import settings
151
+ from golem_faucet import PowFaucetClient
152
+ from web3 import Web3
153
+ try:
154
+ addr = settings.PROVIDER_ID
155
+ faucet = PowFaucetClient(settings.L2_FAUCET_URL, settings.L2_CAPTCHA_URL, settings.L2_CAPTCHA_API_KEY)
156
+ # Check current L2 balance
157
+ w3 = Web3(Web3.HTTPProvider(settings.POLYGON_RPC_URL))
158
+ bal = 0.0
159
+ try:
160
+ bal = float(w3.from_wei(w3.eth.get_balance(Web3.to_checksum_address(addr)), 'ether'))
161
+ except Exception:
162
+ pass
163
+ if bal > 0.01:
164
+ print(f"Sufficient L2 funds ({bal} ETH); skipping faucet.")
165
+ return
166
+ async def _run():
167
+ chall = await faucet.get_challenge()
168
+ if not chall:
169
+ print("Failed to get challenge")
170
+ raise typer.Exit(code=1)
171
+ sols = []
172
+ for salt, target in chall.get('challenge') or []:
173
+ sols.append((salt, target, PowFaucetClient.solve_challenge(salt, target)))
174
+ redeemed = await faucet.redeem(chall.get('token'), sols)
175
+ if not redeemed:
176
+ print("Failed to redeem solutions")
177
+ raise typer.Exit(code=1)
178
+ tx = await faucet.request_funds(addr, redeemed)
179
+ if tx:
180
+ print(f"Faucet tx: {tx}")
181
+ else:
182
+ print("Faucet request failed")
183
+ asyncio.run(_run())
184
+ except Exception as e:
185
+ print(f"Error: {e}")
186
+ raise typer.Exit(code=1)
187
+
144
188
  @cli.command()
145
- def start(no_verify_port: bool = typer.Option(False, "--no-verify-port", help="Skip provider port verification.")):
189
+ def start(
190
+ no_verify_port: bool = typer.Option(False, "--no-verify-port", help="Skip provider port verification."),
191
+ network: str = typer.Option(None, "--network", help="Target network: 'testnet' or 'mainnet' (overrides env)")
192
+ ):
146
193
  """Start the provider server."""
147
- run_server(dev_mode=False, no_verify_port=no_verify_port)
194
+ run_server(dev_mode=False, no_verify_port=no_verify_port, network=network)
148
195
 
149
- @cli.command()
150
- def dev(no_verify_port: bool = typer.Option(True, "--no-verify-port", help="Skip provider port verification.")):
151
- """Start the provider server in development mode."""
152
- run_server(dev_mode=True, no_verify_port=no_verify_port)
196
+ # Removed separate 'dev' command; use environment GOLEM_PROVIDER_ENVIRONMENT=development instead.
153
197
 
154
198
  def _env_path_for(dev_mode: Optional[bool]) -> str:
155
199
  from pathlib import Path
@@ -209,22 +253,35 @@ def _print_pricing_examples(glm_usd):
209
253
  f"- {name} ({res.cpu}C, {res.memory}GB RAM, {res.storage}GB Disk): ~{usd_str} per month (~{glm_str})"
210
254
  )
211
255
 
212
- def run_server(dev_mode: bool, no_verify_port: bool):
256
+ def run_server(dev_mode: bool | None = None, no_verify_port: bool = False, network: str | None = None):
213
257
  """Helper to run the uvicorn server."""
214
258
  import sys
215
259
  from pathlib import Path
216
260
  from dotenv import load_dotenv
217
261
  import uvicorn
218
- # Load appropriate .env file
262
+ # Decide dev mode from explicit arg or environment
263
+ if dev_mode is None:
264
+ dev_mode = os.environ.get("GOLEM_PROVIDER_ENVIRONMENT", "").lower() == "development"
265
+
266
+ # Load appropriate .env file based on mode
219
267
  env_file = ".env.dev" if dev_mode else ".env"
220
268
  env_path = Path(__file__).parent.parent / env_file
221
269
  load_dotenv(dotenv_path=env_path)
270
+
271
+ # Apply network override early (affects settings and annotations)
272
+ if network:
273
+ os.environ["GOLEM_PROVIDER_NETWORK"] = network
222
274
 
223
275
  # The logic for setting the public IP in dev mode is now handled in config.py
224
276
  # The following lines are no longer needed and have been removed.
225
277
 
226
278
  # Import settings after loading env
227
279
  from .config import settings
280
+ if network:
281
+ try:
282
+ settings.NETWORK = network
283
+ except Exception:
284
+ pass
228
285
 
229
286
  # Configure logging with debug mode
230
287
  logger = setup_logger(__name__, debug=dev_mode)
@@ -235,6 +292,8 @@ def run_server(dev_mode: bool, no_verify_port: bool):
235
292
  for key, value in os.environ.items():
236
293
  if key.startswith('GOLEM_PROVIDER_'):
237
294
  logger.info(f"{key}={value}")
295
+ if network:
296
+ logger.info(f"Overridden network: {network}")
238
297
 
239
298
  # Check requirements
240
299
  if not check_requirements():
@@ -257,7 +316,7 @@ def run_server(dev_mode: bool, no_verify_port: bool):
257
316
  "provider:app",
258
317
  host=settings.HOST,
259
318
  port=settings.PORT,
260
- reload=settings.DEBUG,
319
+ reload=dev_mode,
261
320
  log_level="debug" if dev_mode else "info",
262
321
  log_config=log_config,
263
322
  timeout_keep_alive=60, # Increase keep-alive timeout
@@ -5,53 +5,10 @@ from typing import Any, Dict
5
5
 
6
6
  from web3 import Web3
7
7
  from eth_account import Account
8
+ from golem_streaming_abi import STREAM_PAYMENT_ABI
8
9
 
9
10
 
10
- STREAM_PAYMENT_ABI = [
11
- {
12
- "inputs": [
13
- {"internalType": "address", "name": "token", "type": "address"},
14
- {"internalType": "address", "name": "recipient", "type": "address"},
15
- {"internalType": "uint256", "name": "deposit", "type": "uint256"},
16
- {"internalType": "uint128", "name": "ratePerSecond", "type": "uint128"},
17
- ],
18
- "name": "createStream",
19
- "outputs": [{"internalType": "uint256", "name": "streamId", "type": "uint256"}],
20
- "stateMutability": "nonpayable",
21
- "type": "function",
22
- },
23
- {
24
- "inputs": [{"internalType": "uint256", "name": "streamId", "type": "uint256"}],
25
- "name": "withdraw",
26
- "outputs": [],
27
- "stateMutability": "nonpayable",
28
- "type": "function",
29
- },
30
- {
31
- "inputs": [{"internalType": "uint256", "name": "streamId", "type": "uint256"}],
32
- "name": "terminate",
33
- "outputs": [],
34
- "stateMutability": "nonpayable",
35
- "type": "function",
36
- },
37
- {
38
- "inputs": [{"internalType": "uint256", "name": "", "type": "uint256"}],
39
- "name": "streams",
40
- "outputs": [
41
- {"internalType": "address", "name": "token", "type": "address"},
42
- {"internalType": "address", "name": "sender", "type": "address"},
43
- {"internalType": "address", "name": "recipient", "type": "address"},
44
- {"internalType": "uint128", "name": "startTime", "type": "uint128"},
45
- {"internalType": "uint128", "name": "stopTime", "type": "uint128"},
46
- {"internalType": "uint128", "name": "ratePerSecond", "type": "uint128"},
47
- {"internalType": "uint256", "name": "deposit", "type": "uint256"},
48
- {"internalType": "uint256", "name": "withdrawn", "type": "uint256"},
49
- {"internalType": "bool", "name": "halted", "type": "bool"}
50
- ],
51
- "stateMutability": "view",
52
- "type": "function"
53
- },
54
- ]
11
+ # ABI imported from shared package
55
12
 
56
13
 
57
14
  @dataclass
@@ -89,6 +46,10 @@ class StreamPaymentClient:
89
46
  fn = self.contract.functions.withdraw(int(stream_id))
90
47
  receipt = self._send(fn)
91
48
  return receipt["transactionHash"]
49
+ def terminate(self, stream_id: int) -> str:
50
+ fn = self.contract.functions.terminate(int(stream_id))
51
+ receipt = self._send(fn)
52
+ return receipt["transactionHash"]
92
53
 
93
54
  class StreamPaymentReader:
94
55
  def __init__(self, rpc_url: str, contract_address: str):
@@ -129,7 +90,4 @@ class StreamPaymentReader:
129
90
  return False, "stream halted"
130
91
  return True, "ok"
131
92
 
132
- def terminate(self, stream_id: int) -> str:
133
- fn = self.contract.functions.terminate(int(stream_id))
134
- receipt = self._send(fn)
135
- return receipt["transactionHash"]
93
+ # Reader should remain read-only; no terminate here
@@ -52,13 +52,16 @@ class StreamMonitor:
52
52
  if self.settings.STREAM_WITHDRAW_ENABLED and self.client:
53
53
  vested = max(min(now, s["stopTime"]) - s["startTime"], 0) * s["ratePerSecond"]
54
54
  withdrawable = max(vested - s["withdrawn"], 0)
55
- if withdrawable >= self.settings.STREAM_MIN_WITHDRAW_WEI:
55
+ # Enforce a minimum interval between withdrawals
56
+ if withdrawable >= self.settings.STREAM_MIN_WITHDRAW_WEI and (
57
+ now - last_withdraw >= self.settings.STREAM_WITHDRAW_INTERVAL_SECONDS
58
+ ):
56
59
  try:
57
60
  self.client.withdraw(stream_id)
61
+ last_withdraw = now
58
62
  except Exception as e:
59
63
  logger.warning(f"withdraw failed for {stream_id}: {e}")
60
64
  except asyncio.CancelledError:
61
65
  break
62
66
  except Exception as e:
63
67
  logger.error(f"stream monitor error: {e}")
64
-
@@ -1,10 +1,9 @@
1
1
  import asyncio
2
- import hashlib
3
- import httpx
4
- from typing import Optional
2
+ from typing import Optional, List, Tuple
5
3
 
6
4
  from golem_base_sdk import GolemBaseClient
7
5
  from provider.utils.logging import setup_logger
6
+ from golem_faucet import PowFaucetClient
8
7
 
9
8
  logger = setup_logger(__name__)
10
9
 
@@ -13,11 +12,12 @@ class FaucetClient:
13
12
  """A client for interacting with a Proof of Work-protected faucet."""
14
13
 
15
14
  def __init__(self, faucet_url: str, captcha_url: str, captcha_api_key: str):
16
- self.faucet_url = faucet_url
17
- self.captcha_url = captcha_url
15
+ self.faucet_url = faucet_url.rstrip("/")
16
+ self.captcha_url = captcha_url.rstrip("/")
18
17
  self.captcha_api_key = captcha_api_key
19
- self.api_endpoint = f"{faucet_url}/api"
18
+ self.api_endpoint = f"{self.faucet_url}/api"
20
19
  self.client: Optional[GolemBaseClient] = None
20
+ self._pow = PowFaucetClient(self.faucet_url, self.captcha_url, self.captcha_api_key)
21
21
 
22
22
  async def _ensure_client(self):
23
23
  if not self.client:
@@ -82,51 +82,27 @@ class FaucetClient:
82
82
  async def _get_challenge(self) -> Optional[dict]:
83
83
  """Get a PoW challenge from the faucet."""
84
84
  try:
85
- async with httpx.AsyncClient(timeout=60.0) as client:
86
- url = f"{self.captcha_url}/{self.captcha_api_key}/api/challenge"
87
- response = await client.post(url)
88
- response.raise_for_status()
89
- return response.json()
90
- except httpx.HTTPStatusError as e:
91
- logger.error(f"Failed to get PoW challenge: {e.response.text}")
85
+ return await self._pow.get_challenge()
86
+ except Exception as e:
87
+ logger.error(f"Failed to get PoW challenge: {e}")
92
88
  return None
93
89
 
94
90
  def _solve_challenge(self, salt: str, target: str) -> int:
95
91
  """Solve the PoW challenge."""
96
- target_hash = bytes.fromhex(target)
97
- nonce = 0
98
- while True:
99
- hasher = hashlib.sha256()
100
- hasher.update(f"{salt}{nonce}".encode())
101
- if hasher.digest().startswith(target_hash):
102
- return nonce
103
- nonce += 1
92
+ return PowFaucetClient.solve_challenge(salt, target)
104
93
 
105
94
  async def _redeem_solution(self, token: str, solutions: list) -> Optional[str]:
106
95
  """Redeem the PoW solution to get a CAPTCHA token."""
107
96
  try:
108
- async with httpx.AsyncClient(timeout=60.0) as client:
109
- url = f"{self.captcha_url}/{self.captcha_api_key}/api/redeem"
110
- response = await client.post(
111
- url,
112
- json={"token": token, "solutions": solutions}
113
- )
114
- response.raise_for_status()
115
- return response.json().get("token")
116
- except httpx.HTTPStatusError as e:
117
- logger.error(f"Failed to redeem PoW solution: {e.response.text}")
97
+ return await self._pow.redeem(token, solutions)
98
+ except Exception as e:
99
+ logger.error(f"Failed to redeem PoW solution: {e}")
118
100
  return None
119
101
 
120
102
  async def _request_faucet(self, address: str, token: str) -> Optional[str]:
121
103
  """Request funds from the faucet with the CAPTCHA token."""
122
104
  try:
123
- async with httpx.AsyncClient(timeout=60.0) as client:
124
- response = await client.post(
125
- f"{self.api_endpoint}/faucet",
126
- json={"address": address, "captchaToken": token}
127
- )
128
- response.raise_for_status()
129
- return response.json().get("txHash")
130
- except httpx.HTTPStatusError as e:
131
- logger.error(f"Faucet request failed: {e.response.text}")
132
- return None
105
+ return await self._pow.request_funds(address, token)
106
+ except Exception as e:
107
+ logger.error(f"Faucet request failed: {e}")
108
+ return None
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "golem-vm-provider"
3
- version = "0.1.39"
3
+ version = "0.1.42"
4
4
  description = "VM on Golem Provider Node - Run your own provider node to offer VMs on the Golem Network"
5
5
  authors = ["Phillip Jensen <phillip+vm-on-golem@golemgrid.com>"]
6
6
  readme = "README.md"
@@ -21,7 +21,6 @@ packages = [
21
21
 
22
22
  [tool.poetry.scripts]
23
23
  golem-provider = "provider.main:cli"
24
- dev = "provider.main:dev"
25
24
 
26
25
  [tool.poetry.dependencies]
27
26
  python = "^3.11"
@@ -46,10 +45,12 @@ typer = "^0.4.0"
46
45
  web3 = "==7.13.0"
47
46
  golem-base-sdk = "==0.1.0"
48
47
  dependency-injector = "^4.41.0"
48
+ golem-streaming-abi = ">=0.1.0,<0.2.0"
49
+ golem-faucet = ">=0.1.0,<0.2.0"
49
50
 
50
51
  [tool.poetry.group.dev.dependencies]
51
- pytest = "^7.0.0"
52
- pytest-asyncio = "^0.18.0"
52
+ pytest = "^8.2.0"
53
+ pytest-asyncio = "^1.1.0"
53
54
  pytest-mock = "^3.8.2"
54
55
  pytest-cov = "^3.0.0"
55
56
  black = "^22.3.0"