golem-vm-provider 0.1.39__py3-none-any.whl → 0.1.42__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.
- {golem_vm_provider-0.1.39.dist-info → golem_vm_provider-0.1.42.dist-info}/METADATA +51 -8
- {golem_vm_provider-0.1.39.dist-info → golem_vm_provider-0.1.42.dist-info}/RECORD +11 -11
- {golem_vm_provider-0.1.39.dist-info → golem_vm_provider-0.1.42.dist-info}/entry_points.txt +0 -1
- provider/api/routes.py +12 -0
- provider/config.py +34 -6
- provider/discovery/golem_base_advertiser.py +1 -0
- provider/main.py +68 -9
- provider/payments/blockchain_service.py +7 -49
- provider/payments/monitor.py +5 -2
- provider/security/faucet.py +17 -41
- {golem_vm_provider-0.1.39.dist-info → golem_vm_provider-0.1.42.dist-info}/WHEEL +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.3
|
2
2
|
Name: golem-vm-provider
|
3
|
-
Version: 0.1.
|
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 (
|
239
|
+
### Streaming Payments (Native ETH on L2)
|
234
240
|
|
235
|
-
Enable on‑chain stream‑gated rentals
|
241
|
+
Enable on‑chain stream‑gated rentals funded in native ETH. Configure (env prefix `GOLEM_PROVIDER_`):
|
236
242
|
|
237
|
-
- `POLYGON_RPC_URL` —
|
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` —
|
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": "
|
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
|
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
|
@@ -1,24 +1,24 @@
|
|
1
1
|
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=_y3N9amTFaHfH7_3uZlvhPAoUA4HY-93f-BBuC4EoBM,3830
|
4
|
-
provider/api/routes.py,sha256=
|
5
|
-
provider/config.py,sha256=
|
4
|
+
provider/api/routes.py,sha256=DODju_SOgfkyeZv9zLTuAXbyvdaWRbU4kHZpON_GeMg,8288
|
5
|
+
provider/config.py,sha256=u7Y0w9-s8iitQzQ3R0ztgYSfq_WeSRUYzBRTor8GAlY,22666
|
6
6
|
provider/container.py,sha256=81x5LiA-qjYN1Uh_JdOxqvuIXiNDr9X3OXNN0VqYFCI,3681
|
7
7
|
provider/discovery/__init__.py,sha256=Y6o8RxGevBpuQS3k32y-zSVbP6HBXG3veBl9ElVPKaU,349
|
8
8
|
provider/discovery/advertiser.py,sha256=o-LiDl1j0lXMUU0-zPe3qerjpoD2360EA60Y_V_VeBc,6571
|
9
|
-
provider/discovery/golem_base_advertiser.py,sha256=
|
9
|
+
provider/discovery/golem_base_advertiser.py,sha256=0I6V73GL671c8mQNpb92sdyboXzbJuRJdv06QS7MuBQ,6942
|
10
10
|
provider/discovery/golem_base_utils.py,sha256=xk7vznhMgzrn0AuGyk6-9N9ukp9oPdBbbk1RI-sVjp0,607
|
11
11
|
provider/discovery/multi_advertiser.py,sha256=_J79wA1-XQ4GsLzt9KrKpWigGSGBqtut7DaocIk2fyE,991
|
12
12
|
provider/discovery/resource_monitor.py,sha256=AmiEc7yBGEGXCunQ-QKmVgosDX3gOhK1Y58LJZXrwAs,949
|
13
13
|
provider/discovery/resource_tracker.py,sha256=MP7IXd3aIMsjB4xz5Oj9zFDTEnvrnw-Cyxpl33xcJcc,6006
|
14
14
|
provider/discovery/service.py,sha256=vX_mVSxvn3arnb2cKDM_SeJp1ZgPdImP2aUubeXgdRg,915
|
15
|
-
provider/main.py,sha256=
|
15
|
+
provider/main.py,sha256=eS_rqvC9bCXdJfYg-sNYt02SGPdodgPGa350SWcd1os,14271
|
16
16
|
provider/network/port_verifier.py,sha256=3l6WNwBHydggJRFYkAsuBp1eCxaU619kjWuM-zSVj2o,13267
|
17
|
-
provider/payments/blockchain_service.py,sha256=
|
18
|
-
provider/payments/monitor.py,sha256=
|
17
|
+
provider/payments/blockchain_service.py,sha256=Qq01M7N_gzUidnXBngDcNoBRs_sHKaTpvSFKz6wEAMk,3363
|
18
|
+
provider/payments/monitor.py,sha256=6zzW275ktSHcYiIXuyThr-UA-JR7VoHw2Hk1qAUCb-U,3058
|
19
19
|
provider/payments/stream_map.py,sha256=qk6Y8hS72DplAifZ0ZMWPHBAyc_3IWIQyWUBuCU3_To,1191
|
20
20
|
provider/security/ethereum.py,sha256=EwPZj4JR8OEpto6LhKjuuT3Z9pBX6P7-UQaqJtqFkYQ,1242
|
21
|
-
provider/security/faucet.py,sha256=
|
21
|
+
provider/security/faucet.py,sha256=8T4lW1fVQgUk8EQILgbrr9UUosw9e7eA40tlZ2_KCPQ,4368
|
22
22
|
provider/service.py,sha256=IIjeSM9T4r616nBRnxCUum_sgbyRusMMcja3yQd8zQI,3383
|
23
23
|
provider/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
24
24
|
provider/utils/ascii_art.py,sha256=ykBFsztk57GIiz1NJ-EII5UvN74iECqQL4h9VmiW6Z8,3161
|
@@ -37,7 +37,7 @@ provider/vm/port_manager.py,sha256=iYSwjTjD_ziOhG8aI7juKHw1OwwRUTJQyQoRUNQvz9w,1
|
|
37
37
|
provider/vm/provider.py,sha256=A7QN89EJjcSS40_SmKeinG1Jp_NGffJaLse-XdKciAs,1164
|
38
38
|
provider/vm/proxy_manager.py,sha256=n4NTsyz2rtrvjtf_ceKBk-g2q_mzqPwruB1q7UlQVBc,14928
|
39
39
|
provider/vm/service.py,sha256=Ki4SGNIZUq3XmaPMwAOoNzdZzKQsmFXid374wgjFPes,4636
|
40
|
-
golem_vm_provider-0.1.
|
41
|
-
golem_vm_provider-0.1.
|
42
|
-
golem_vm_provider-0.1.
|
43
|
-
golem_vm_provider-0.1.
|
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,,
|
provider/api/routes.py
CHANGED
@@ -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:
|
provider/config.py
CHANGED
@@ -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://
|
141
|
-
description="
|
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="
|
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),
|
provider/main.py
CHANGED
@@ -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(
|
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
|
-
|
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
|
-
#
|
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=
|
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
|
-
|
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
|
-
|
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
|
provider/payments/monitor.py
CHANGED
@@ -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
|
-
|
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
|
-
|
provider/security/faucet.py
CHANGED
@@ -1,10 +1,9 @@
|
|
1
1
|
import asyncio
|
2
|
-
import
|
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
|
-
|
86
|
-
|
87
|
-
|
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
|
-
|
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
|
-
|
109
|
-
|
110
|
-
|
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
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
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
|
File without changes
|