golem-vm-provider 0.1.43__tar.gz → 0.1.44__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.
- {golem_vm_provider-0.1.43 → golem_vm_provider-0.1.44}/PKG-INFO +61 -7
- {golem_vm_provider-0.1.43 → golem_vm_provider-0.1.44}/README.md +60 -6
- {golem_vm_provider-0.1.43 → golem_vm_provider-0.1.44}/provider/api/models.py +28 -0
- {golem_vm_provider-0.1.43 → golem_vm_provider-0.1.44}/provider/api/routes.py +81 -1
- {golem_vm_provider-0.1.43 → golem_vm_provider-0.1.44}/provider/config.py +54 -4
- golem_vm_provider-0.1.44/provider/data/deployments/l2.json +9 -0
- {golem_vm_provider-0.1.43 → golem_vm_provider-0.1.44}/provider/main.py +104 -0
- {golem_vm_provider-0.1.43 → golem_vm_provider-0.1.44}/provider/payments/monitor.py +10 -0
- {golem_vm_provider-0.1.43 → golem_vm_provider-0.1.44}/pyproject.toml +6 -1
- {golem_vm_provider-0.1.43 → golem_vm_provider-0.1.44}/provider/__init__.py +0 -0
- {golem_vm_provider-0.1.43 → golem_vm_provider-0.1.44}/provider/api/__init__.py +0 -0
- {golem_vm_provider-0.1.43 → golem_vm_provider-0.1.44}/provider/container.py +0 -0
- {golem_vm_provider-0.1.43 → golem_vm_provider-0.1.44}/provider/discovery/__init__.py +0 -0
- {golem_vm_provider-0.1.43 → golem_vm_provider-0.1.44}/provider/discovery/advertiser.py +0 -0
- {golem_vm_provider-0.1.43 → golem_vm_provider-0.1.44}/provider/discovery/golem_base_advertiser.py +0 -0
- {golem_vm_provider-0.1.43 → golem_vm_provider-0.1.44}/provider/discovery/golem_base_utils.py +0 -0
- {golem_vm_provider-0.1.43 → golem_vm_provider-0.1.44}/provider/discovery/multi_advertiser.py +0 -0
- {golem_vm_provider-0.1.43 → golem_vm_provider-0.1.44}/provider/discovery/resource_monitor.py +0 -0
- {golem_vm_provider-0.1.43 → golem_vm_provider-0.1.44}/provider/discovery/resource_tracker.py +0 -0
- {golem_vm_provider-0.1.43 → golem_vm_provider-0.1.44}/provider/discovery/service.py +0 -0
- {golem_vm_provider-0.1.43 → golem_vm_provider-0.1.44}/provider/network/port_verifier.py +0 -0
- {golem_vm_provider-0.1.43 → golem_vm_provider-0.1.44}/provider/payments/blockchain_service.py +0 -0
- {golem_vm_provider-0.1.43 → golem_vm_provider-0.1.44}/provider/payments/stream_map.py +0 -0
- {golem_vm_provider-0.1.43 → golem_vm_provider-0.1.44}/provider/security/ethereum.py +0 -0
- {golem_vm_provider-0.1.43 → golem_vm_provider-0.1.44}/provider/security/faucet.py +0 -0
- {golem_vm_provider-0.1.43 → golem_vm_provider-0.1.44}/provider/service.py +0 -0
- {golem_vm_provider-0.1.43 → golem_vm_provider-0.1.44}/provider/utils/__init__.py +0 -0
- {golem_vm_provider-0.1.43 → golem_vm_provider-0.1.44}/provider/utils/ascii_art.py +0 -0
- {golem_vm_provider-0.1.43 → golem_vm_provider-0.1.44}/provider/utils/logging.py +0 -0
- {golem_vm_provider-0.1.43 → golem_vm_provider-0.1.44}/provider/utils/port_display.py +0 -0
- {golem_vm_provider-0.1.43 → golem_vm_provider-0.1.44}/provider/utils/pricing.py +0 -0
- {golem_vm_provider-0.1.43 → golem_vm_provider-0.1.44}/provider/utils/retry.py +0 -0
- {golem_vm_provider-0.1.43 → golem_vm_provider-0.1.44}/provider/utils/setup.py +0 -0
- {golem_vm_provider-0.1.43 → golem_vm_provider-0.1.44}/provider/vm/__init__.py +0 -0
- {golem_vm_provider-0.1.43 → golem_vm_provider-0.1.44}/provider/vm/cloud_init.py +0 -0
- {golem_vm_provider-0.1.43 → golem_vm_provider-0.1.44}/provider/vm/models.py +0 -0
- {golem_vm_provider-0.1.43 → golem_vm_provider-0.1.44}/provider/vm/multipass.py +0 -0
- {golem_vm_provider-0.1.43 → golem_vm_provider-0.1.44}/provider/vm/multipass_adapter.py +0 -0
- {golem_vm_provider-0.1.43 → golem_vm_provider-0.1.44}/provider/vm/name_mapper.py +0 -0
- {golem_vm_provider-0.1.43 → golem_vm_provider-0.1.44}/provider/vm/port_manager.py +0 -0
- {golem_vm_provider-0.1.43 → golem_vm_provider-0.1.44}/provider/vm/provider.py +0 -0
- {golem_vm_provider-0.1.43 → golem_vm_provider-0.1.44}/provider/vm/proxy_manager.py +0 -0
- {golem_vm_provider-0.1.43 → golem_vm_provider-0.1.44}/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.
|
3
|
+
Version: 0.1.44
|
4
4
|
Summary: VM on Golem Provider Node - Run your own provider node to offer VMs on the Golem Network
|
5
5
|
Keywords: golem,vm,provider,cloud,decentralized
|
6
6
|
Author: Phillip Jensen
|
@@ -238,11 +238,12 @@ GOLEM_PROVIDER_NETWORK="testnet" # or "mainnet"
|
|
238
238
|
|
239
239
|
### Streaming Payments (Native ETH on L2)
|
240
240
|
|
241
|
-
Enable on‑chain stream‑gated rentals funded in native ETH. Configure (env prefix `GOLEM_PROVIDER_`):
|
241
|
+
Enable on‑chain stream‑gated rentals funded in native ETH. By default, the provider auto‑loads the StreamPayment contract from `contracts/deployments/l2.json` and enables payments out of the box. Configure/override (env prefix `GOLEM_PROVIDER_`):
|
242
242
|
|
243
|
-
- `POLYGON_RPC_URL` — EVM RPC URL (default
|
244
|
-
- `STREAM_PAYMENT_ADDRESS` — StreamPayment
|
245
|
-
- `GLM_TOKEN_ADDRESS` — Token address
|
243
|
+
- `POLYGON_RPC_URL` — EVM RPC URL (default L2 RPC)
|
244
|
+
- `STREAM_PAYMENT_ADDRESS` — StreamPayment address (defaults from `contracts/deployments/l2.json`)
|
245
|
+
- `GLM_TOKEN_ADDRESS` — Token address (defaults from `contracts/deployments/l2.json`; `0x0` means native ETH)
|
246
|
+
- Optional override of deployments directory: set `GOLEM_DEPLOYMENTS_DIR` to a folder containing `l2.json`.
|
246
247
|
|
247
248
|
Optional background automation (all disabled by default):
|
248
249
|
|
@@ -255,7 +256,7 @@ Optional background automation (all disabled by default):
|
|
255
256
|
|
256
257
|
Implementation notes:
|
257
258
|
|
258
|
-
- The provider exposes `GET /api/v1/provider/info` returning `provider_id`, `stream_payment_address`, and `glm_token_address`.
|
259
|
+
- The provider exposes `GET /api/v1/provider/info` returning `provider_id`, `stream_payment_address`, and `glm_token_address`. Requestors should prefer these values when opening streams.
|
259
260
|
- On successful VM creation with a valid `stream_id`, the provider persists a VM→stream mapping in `streams.json`. This enables the background monitor to stop VMs with low remaining runway and to withdraw vested funds according to configured intervals.
|
260
261
|
- When a VM is deleted, the VM→stream mapping is cleaned up.
|
261
262
|
|
@@ -328,7 +329,44 @@ Response:
|
|
328
329
|
```
|
329
330
|
|
330
331
|
Use this endpoint to discover the correct recipient for creating a GLM stream.
|
331
|
-
|
332
|
+
|
333
|
+
### Payment Streams
|
334
|
+
|
335
|
+
- Get a VM’s stream status: `GET /api/v1/vms/{vm_id}/stream`
|
336
|
+
- List all mapped streams: `GET /api/v1/payments/streams`
|
337
|
+
|
338
|
+
Response (per stream):
|
339
|
+
|
340
|
+
```json
|
341
|
+
{
|
342
|
+
"vm_id": "golem-my-webserver-20250219-130424",
|
343
|
+
"stream_id": 123,
|
344
|
+
"verified": true,
|
345
|
+
"reason": "ok",
|
346
|
+
"chain": {
|
347
|
+
"token": "0x0000000000000000000000000000000000000000",
|
348
|
+
"sender": "0x...",
|
349
|
+
"recipient": "0xProviderEthereumAddress",
|
350
|
+
"startTime": 1700000000,
|
351
|
+
"stopTime": 1700007200,
|
352
|
+
"ratePerSecond": 12345,
|
353
|
+
"deposit": 1000000000000000000,
|
354
|
+
"withdrawn": 0,
|
355
|
+
"halted": false
|
356
|
+
},
|
357
|
+
"computed": {
|
358
|
+
"now": 1700003600,
|
359
|
+
"remaining_seconds": 3600,
|
360
|
+
"vested_wei": 44442000,
|
361
|
+
"withdrawable_wei": 44442000
|
362
|
+
}
|
363
|
+
}
|
364
|
+
```
|
365
|
+
|
366
|
+
Notes:
|
367
|
+
- Endpoints return 400 when streaming is disabled (zero `STREAM_PAYMENT_ADDRESS`).
|
368
|
+
- In development mode (`GOLEM_PROVIDER_ENVIRONMENT=development`) additional debug logs are emitted around stream verification and monitor ticks.
|
369
|
+
|
332
370
|
## Operations
|
333
371
|
|
334
372
|
### Starting the Provider
|
@@ -382,6 +420,22 @@ Defaults:
|
|
382
420
|
- CAPTCHA: `https://cap.gobas.me/05381a2cef5e`
|
383
421
|
- Override with env: `GOLEM_PROVIDER_L2_FAUCET_URL`, `GOLEM_PROVIDER_L2_CAPTCHA_URL`, `GOLEM_PROVIDER_L2_CAPTCHA_API_KEY`.
|
384
422
|
|
423
|
+
### Streams (CLI)
|
424
|
+
|
425
|
+
- List all mapped streams with computed fields:
|
426
|
+
|
427
|
+
```bash
|
428
|
+
poetry run golem-provider streams list
|
429
|
+
# or JSON
|
430
|
+
poetry run golem-provider streams list --json
|
431
|
+
```
|
432
|
+
|
433
|
+
- Show one VM’s stream (VM id = `requestor_name`):
|
434
|
+
|
435
|
+
```bash
|
436
|
+
poetry run golem-provider streams show <vm_id>
|
437
|
+
```
|
438
|
+
|
385
439
|
### Resource Advertisement Flow
|
386
440
|
|
387
441
|
```mermaid
|
@@ -193,11 +193,12 @@ GOLEM_PROVIDER_NETWORK="testnet" # or "mainnet"
|
|
193
193
|
|
194
194
|
### Streaming Payments (Native ETH on L2)
|
195
195
|
|
196
|
-
Enable on‑chain stream‑gated rentals funded in native ETH. Configure (env prefix `GOLEM_PROVIDER_`):
|
196
|
+
Enable on‑chain stream‑gated rentals funded in native ETH. By default, the provider auto‑loads the StreamPayment contract from `contracts/deployments/l2.json` and enables payments out of the box. Configure/override (env prefix `GOLEM_PROVIDER_`):
|
197
197
|
|
198
|
-
- `POLYGON_RPC_URL` — EVM RPC URL (default
|
199
|
-
- `STREAM_PAYMENT_ADDRESS` — StreamPayment
|
200
|
-
- `GLM_TOKEN_ADDRESS` — Token address
|
198
|
+
- `POLYGON_RPC_URL` — EVM RPC URL (default L2 RPC)
|
199
|
+
- `STREAM_PAYMENT_ADDRESS` — StreamPayment address (defaults from `contracts/deployments/l2.json`)
|
200
|
+
- `GLM_TOKEN_ADDRESS` — Token address (defaults from `contracts/deployments/l2.json`; `0x0` means native ETH)
|
201
|
+
- Optional override of deployments directory: set `GOLEM_DEPLOYMENTS_DIR` to a folder containing `l2.json`.
|
201
202
|
|
202
203
|
Optional background automation (all disabled by default):
|
203
204
|
|
@@ -210,7 +211,7 @@ Optional background automation (all disabled by default):
|
|
210
211
|
|
211
212
|
Implementation notes:
|
212
213
|
|
213
|
-
- The provider exposes `GET /api/v1/provider/info` returning `provider_id`, `stream_payment_address`, and `glm_token_address`.
|
214
|
+
- The provider exposes `GET /api/v1/provider/info` returning `provider_id`, `stream_payment_address`, and `glm_token_address`. Requestors should prefer these values when opening streams.
|
214
215
|
- 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
216
|
- When a VM is deleted, the VM→stream mapping is cleaned up.
|
216
217
|
|
@@ -283,7 +284,44 @@ Response:
|
|
283
284
|
```
|
284
285
|
|
285
286
|
Use this endpoint to discover the correct recipient for creating a GLM stream.
|
286
|
-
|
287
|
+
|
288
|
+
### Payment Streams
|
289
|
+
|
290
|
+
- Get a VM’s stream status: `GET /api/v1/vms/{vm_id}/stream`
|
291
|
+
- List all mapped streams: `GET /api/v1/payments/streams`
|
292
|
+
|
293
|
+
Response (per stream):
|
294
|
+
|
295
|
+
```json
|
296
|
+
{
|
297
|
+
"vm_id": "golem-my-webserver-20250219-130424",
|
298
|
+
"stream_id": 123,
|
299
|
+
"verified": true,
|
300
|
+
"reason": "ok",
|
301
|
+
"chain": {
|
302
|
+
"token": "0x0000000000000000000000000000000000000000",
|
303
|
+
"sender": "0x...",
|
304
|
+
"recipient": "0xProviderEthereumAddress",
|
305
|
+
"startTime": 1700000000,
|
306
|
+
"stopTime": 1700007200,
|
307
|
+
"ratePerSecond": 12345,
|
308
|
+
"deposit": 1000000000000000000,
|
309
|
+
"withdrawn": 0,
|
310
|
+
"halted": false
|
311
|
+
},
|
312
|
+
"computed": {
|
313
|
+
"now": 1700003600,
|
314
|
+
"remaining_seconds": 3600,
|
315
|
+
"vested_wei": 44442000,
|
316
|
+
"withdrawable_wei": 44442000
|
317
|
+
}
|
318
|
+
}
|
319
|
+
```
|
320
|
+
|
321
|
+
Notes:
|
322
|
+
- Endpoints return 400 when streaming is disabled (zero `STREAM_PAYMENT_ADDRESS`).
|
323
|
+
- In development mode (`GOLEM_PROVIDER_ENVIRONMENT=development`) additional debug logs are emitted around stream verification and monitor ticks.
|
324
|
+
|
287
325
|
## Operations
|
288
326
|
|
289
327
|
### Starting the Provider
|
@@ -337,6 +375,22 @@ Defaults:
|
|
337
375
|
- CAPTCHA: `https://cap.gobas.me/05381a2cef5e`
|
338
376
|
- Override with env: `GOLEM_PROVIDER_L2_FAUCET_URL`, `GOLEM_PROVIDER_L2_CAPTCHA_URL`, `GOLEM_PROVIDER_L2_CAPTCHA_API_KEY`.
|
339
377
|
|
378
|
+
### Streams (CLI)
|
379
|
+
|
380
|
+
- List all mapped streams with computed fields:
|
381
|
+
|
382
|
+
```bash
|
383
|
+
poetry run golem-provider streams list
|
384
|
+
# or JSON
|
385
|
+
poetry run golem-provider streams list --json
|
386
|
+
```
|
387
|
+
|
388
|
+
- Show one VM’s stream (VM id = `requestor_name`):
|
389
|
+
|
390
|
+
```bash
|
391
|
+
poetry run golem-provider streams show <vm_id>
|
392
|
+
```
|
393
|
+
|
340
394
|
### Resource Advertisement Flow
|
341
395
|
|
342
396
|
```mermaid
|
@@ -116,3 +116,31 @@ class ProviderInfoResponse(BaseModel):
|
|
116
116
|
provider_id: str
|
117
117
|
stream_payment_address: str
|
118
118
|
glm_token_address: str
|
119
|
+
|
120
|
+
|
121
|
+
class StreamOnChain(BaseModel):
|
122
|
+
token: str
|
123
|
+
sender: str
|
124
|
+
recipient: str
|
125
|
+
startTime: int
|
126
|
+
stopTime: int
|
127
|
+
ratePerSecond: int
|
128
|
+
deposit: int
|
129
|
+
withdrawn: int
|
130
|
+
halted: bool
|
131
|
+
|
132
|
+
|
133
|
+
class StreamComputed(BaseModel):
|
134
|
+
now: int
|
135
|
+
remaining_seconds: int
|
136
|
+
vested_wei: int
|
137
|
+
withdrawable_wei: int
|
138
|
+
|
139
|
+
|
140
|
+
class StreamStatus(BaseModel):
|
141
|
+
vm_id: str
|
142
|
+
stream_id: int
|
143
|
+
chain: StreamOnChain
|
144
|
+
computed: StreamComputed
|
145
|
+
verified: bool
|
146
|
+
reason: str
|
@@ -11,7 +11,7 @@ from ..container import Container
|
|
11
11
|
from ..utils.logging import setup_logger
|
12
12
|
from ..utils.ascii_art import vm_creation_animation, vm_status_change
|
13
13
|
from ..vm.models import VMInfo, VMAccessInfo, VMConfig, VMResources, VMNotFoundError
|
14
|
-
from .models import CreateVMRequest, ProviderInfoResponse
|
14
|
+
from .models import CreateVMRequest, ProviderInfoResponse, StreamStatus, StreamOnChain, StreamComputed
|
15
15
|
from ..payments.blockchain_service import StreamPaymentReader
|
16
16
|
from ..vm.service import VMService
|
17
17
|
from ..vm.multipass_adapter import MultipassError
|
@@ -40,6 +40,17 @@ async def create_vm(
|
|
40
40
|
raise HTTPException(status_code=400, detail="stream_id required when payments are enabled")
|
41
41
|
reader = StreamPaymentReader(settings["POLYGON_RPC_URL"], settings["STREAM_PAYMENT_ADDRESS"])
|
42
42
|
ok, reason = reader.verify_stream(int(request.stream_id), settings["PROVIDER_ID"])
|
43
|
+
try:
|
44
|
+
s = reader.get_stream(int(request.stream_id))
|
45
|
+
now = int(reader.web3.eth.get_block("latest")["timestamp"]) # type: ignore[attr-defined]
|
46
|
+
remaining = max(int(s["stopTime"]) - now, 0)
|
47
|
+
logger.info(
|
48
|
+
f"💸 Stream check id={int(request.stream_id)} ok={ok} reason='{reason}' "
|
49
|
+
f"start={s['startTime']} stop={s['stopTime']} rate={s['ratePerSecond']} deposit={s['deposit']} withdrawn={s['withdrawn']} remaining={remaining}s"
|
50
|
+
)
|
51
|
+
except Exception:
|
52
|
+
# Best-effort logging; creation will continue/fail based on ok
|
53
|
+
pass
|
43
54
|
if not ok:
|
44
55
|
raise HTTPException(status_code=400, detail=f"invalid stream: {reason}")
|
45
56
|
|
@@ -201,3 +212,72 @@ async def provider_info(settings: Settings = Depends(Provide[Container.config]))
|
|
201
212
|
stream_payment_address=settings["STREAM_PAYMENT_ADDRESS"],
|
202
213
|
glm_token_address=settings["GLM_TOKEN_ADDRESS"],
|
203
214
|
)
|
215
|
+
|
216
|
+
|
217
|
+
@router.get("/vms/{requestor_name}/stream", response_model=StreamStatus)
|
218
|
+
@inject
|
219
|
+
async def get_vm_stream_status(
|
220
|
+
requestor_name: str,
|
221
|
+
settings: Settings = Depends(Provide[Container.config]),
|
222
|
+
stream_map = Depends(Provide[Container.stream_map]),
|
223
|
+
) -> StreamStatus:
|
224
|
+
"""Return on-chain stream status for a VM (if mapped)."""
|
225
|
+
if not settings["STREAM_PAYMENT_ADDRESS"] or settings["STREAM_PAYMENT_ADDRESS"] == "0x0000000000000000000000000000000000000000":
|
226
|
+
raise HTTPException(status_code=400, detail="streaming payments not enabled on this provider")
|
227
|
+
stream_id = await stream_map.get(requestor_name)
|
228
|
+
if stream_id is None:
|
229
|
+
raise HTTPException(status_code=404, detail="no stream mapped for this VM")
|
230
|
+
reader = StreamPaymentReader(settings["POLYGON_RPC_URL"], settings["STREAM_PAYMENT_ADDRESS"])
|
231
|
+
try:
|
232
|
+
s = reader.get_stream(int(stream_id))
|
233
|
+
except Exception as e:
|
234
|
+
raise HTTPException(status_code=502, detail=f"stream lookup failed: {e}")
|
235
|
+
ok, reason = reader.verify_stream(int(stream_id), settings["PROVIDER_ID"])
|
236
|
+
now = int(reader.web3.eth.get_block("latest")["timestamp"]) # type: ignore[attr-defined]
|
237
|
+
vested = max(min(now, int(s["stopTime"])) - int(s["startTime"]), 0) * int(s["ratePerSecond"]) # type: ignore[operator]
|
238
|
+
withdrawable = max(int(vested) - int(s["withdrawn"]), 0)
|
239
|
+
remaining = max(int(s["stopTime"]) - now, 0)
|
240
|
+
return StreamStatus(
|
241
|
+
vm_id=requestor_name,
|
242
|
+
stream_id=int(stream_id),
|
243
|
+
chain=StreamOnChain(**s),
|
244
|
+
computed=StreamComputed(now=now, remaining_seconds=remaining, vested_wei=int(vested), withdrawable_wei=int(withdrawable)),
|
245
|
+
verified=bool(ok),
|
246
|
+
reason=str(reason),
|
247
|
+
)
|
248
|
+
|
249
|
+
|
250
|
+
@router.get("/payments/streams", response_model=List[StreamStatus])
|
251
|
+
@inject
|
252
|
+
async def list_stream_statuses(
|
253
|
+
settings: Settings = Depends(Provide[Container.config]),
|
254
|
+
stream_map = Depends(Provide[Container.stream_map]),
|
255
|
+
) -> List[StreamStatus]:
|
256
|
+
"""List stream status for all mapped VMs."""
|
257
|
+
if not settings["STREAM_PAYMENT_ADDRESS"] or settings["STREAM_PAYMENT_ADDRESS"] == "0x0000000000000000000000000000000000000000":
|
258
|
+
raise HTTPException(status_code=400, detail="streaming payments not enabled on this provider")
|
259
|
+
reader = StreamPaymentReader(settings["POLYGON_RPC_URL"], settings["STREAM_PAYMENT_ADDRESS"])
|
260
|
+
items = await stream_map.all_items()
|
261
|
+
now = int(reader.web3.eth.get_block("latest")["timestamp"]) if items else 0 # type: ignore[attr-defined]
|
262
|
+
resp: List[StreamStatus] = []
|
263
|
+
for vm_id, stream_id in items.items():
|
264
|
+
try:
|
265
|
+
s = reader.get_stream(int(stream_id))
|
266
|
+
ok, reason = reader.verify_stream(int(stream_id), settings["PROVIDER_ID"])
|
267
|
+
vested = max(min(now, int(s["stopTime"])) - int(s["startTime"]), 0) * int(s["ratePerSecond"]) # type: ignore[operator]
|
268
|
+
withdrawable = max(int(vested) - int(s["withdrawn"]), 0)
|
269
|
+
remaining = max(int(s["stopTime"]) - now, 0)
|
270
|
+
resp.append(
|
271
|
+
StreamStatus(
|
272
|
+
vm_id=vm_id,
|
273
|
+
stream_id=int(stream_id),
|
274
|
+
chain=StreamOnChain(**s),
|
275
|
+
computed=StreamComputed(now=now, remaining_seconds=remaining, vested_wei=int(vested), withdrawable_wei=int(withdrawable)),
|
276
|
+
verified=bool(ok),
|
277
|
+
reason=str(reason),
|
278
|
+
)
|
279
|
+
)
|
280
|
+
except Exception as e:
|
281
|
+
logger.warning(f"stream {stream_id} lookup failed: {e}")
|
282
|
+
continue
|
283
|
+
return resp
|
@@ -1,4 +1,5 @@
|
|
1
1
|
import os
|
2
|
+
import json
|
2
3
|
from pathlib import Path
|
3
4
|
from typing import Optional
|
4
5
|
import uuid
|
@@ -141,12 +142,12 @@ class Settings(BaseSettings):
|
|
141
142
|
description="EVM RPC URL for streaming payments (L2 by default)"
|
142
143
|
)
|
143
144
|
STREAM_PAYMENT_ADDRESS: str = Field(
|
144
|
-
default="
|
145
|
-
description="Deployed StreamPayment contract address"
|
145
|
+
default="",
|
146
|
+
description="Deployed StreamPayment contract address (defaults to contracts/deployments/l2.json)"
|
146
147
|
)
|
147
148
|
GLM_TOKEN_ADDRESS: str = Field(
|
148
|
-
default="
|
149
|
-
description="Token address (0x0 means native ETH)"
|
149
|
+
default="",
|
150
|
+
description="Token address (0x0 means native ETH). Defaults from l2.json"
|
150
151
|
)
|
151
152
|
STREAM_MIN_REMAINING_SECONDS: int = Field(
|
152
153
|
default=3600,
|
@@ -201,6 +202,55 @@ class Settings(BaseSettings):
|
|
201
202
|
return os.environ[key]
|
202
203
|
return v
|
203
204
|
|
205
|
+
@staticmethod
|
206
|
+
def _load_l2_deployment() -> tuple[str | None, str | None]:
|
207
|
+
"""Try to load default StreamPayment + token from contracts/deployments/l2.json.
|
208
|
+
|
209
|
+
Returns (stream_payment_address, glm_token_address) or (None, None) if not found.
|
210
|
+
"""
|
211
|
+
try:
|
212
|
+
# Allow override via env
|
213
|
+
base = os.environ.get("GOLEM_DEPLOYMENTS_DIR")
|
214
|
+
if base:
|
215
|
+
path = Path(base) / "l2.json"
|
216
|
+
else:
|
217
|
+
# repo root = ../../ from this file
|
218
|
+
path = Path(__file__).resolve().parents[2] / "contracts" / "deployments" / "l2.json"
|
219
|
+
if not path.exists():
|
220
|
+
# Try package resource fallback
|
221
|
+
try:
|
222
|
+
import importlib.resources as ir
|
223
|
+
with ir.files("provider.data.deployments").joinpath("l2.json").open("r") as fh: # type: ignore[attr-defined]
|
224
|
+
data = json.load(fh)
|
225
|
+
except Exception:
|
226
|
+
return None, None
|
227
|
+
else:
|
228
|
+
data = json.loads(path.read_text())
|
229
|
+
sp = data.get("StreamPayment", {})
|
230
|
+
addr = sp.get("address")
|
231
|
+
token = sp.get("glmToken")
|
232
|
+
if isinstance(addr, str) and addr:
|
233
|
+
return addr, token or "0x0000000000000000000000000000000000000000"
|
234
|
+
except Exception:
|
235
|
+
pass
|
236
|
+
return None, None
|
237
|
+
|
238
|
+
@field_validator("STREAM_PAYMENT_ADDRESS", mode='before')
|
239
|
+
@classmethod
|
240
|
+
def default_stream_addr(cls, v: str) -> str:
|
241
|
+
if v:
|
242
|
+
return v
|
243
|
+
addr, _ = Settings._load_l2_deployment()
|
244
|
+
return addr or "0x0000000000000000000000000000000000000000"
|
245
|
+
|
246
|
+
@field_validator("GLM_TOKEN_ADDRESS", mode='before')
|
247
|
+
@classmethod
|
248
|
+
def default_token_addr(cls, v: str) -> str:
|
249
|
+
if v:
|
250
|
+
return v
|
251
|
+
_, token = Settings._load_l2_deployment()
|
252
|
+
return token or "0x0000000000000000000000000000000000000000"
|
253
|
+
|
204
254
|
# VM Settings
|
205
255
|
MAX_VMS: int = 10
|
206
256
|
DEFAULT_VM_IMAGE: str = "ubuntu:24.04"
|
@@ -123,8 +123,10 @@ except ImportError:
|
|
123
123
|
cli = typer.Typer()
|
124
124
|
pricing_app = typer.Typer(help="Configure USD pricing; auto-converts to GLM.")
|
125
125
|
wallet_app = typer.Typer(help="Wallet utilities (funding, balance)")
|
126
|
+
streams_app = typer.Typer(help="Inspect payment streams")
|
126
127
|
cli.add_typer(pricing_app, name="pricing")
|
127
128
|
cli.add_typer(wallet_app, name="wallet")
|
129
|
+
cli.add_typer(streams_app, name="streams")
|
128
130
|
|
129
131
|
def print_version(ctx: typer.Context, value: bool):
|
130
132
|
if not value:
|
@@ -185,6 +187,108 @@ def wallet_faucet_l2():
|
|
185
187
|
print(f"Error: {e}")
|
186
188
|
raise typer.Exit(code=1)
|
187
189
|
|
190
|
+
|
191
|
+
@streams_app.command("list")
|
192
|
+
def streams_list(json_out: bool = typer.Option(False, "--json", help="Output in JSON")):
|
193
|
+
"""List all mapped streams with computed status."""
|
194
|
+
from .container import Container
|
195
|
+
from .config import settings
|
196
|
+
from .payments.blockchain_service import StreamPaymentReader
|
197
|
+
import json as _json
|
198
|
+
try:
|
199
|
+
if not settings.STREAM_PAYMENT_ADDRESS or settings.STREAM_PAYMENT_ADDRESS == "0x0000000000000000000000000000000000000000":
|
200
|
+
print("Streaming payments are disabled on this provider.")
|
201
|
+
raise typer.Exit(code=1)
|
202
|
+
c = Container()
|
203
|
+
c.config.from_pydantic(settings)
|
204
|
+
stream_map = c.stream_map()
|
205
|
+
reader = StreamPaymentReader(settings.POLYGON_RPC_URL, settings.STREAM_PAYMENT_ADDRESS)
|
206
|
+
items = asyncio.run(stream_map.all_items())
|
207
|
+
now = int(reader.web3.eth.get_block("latest")["timestamp"]) if items else 0
|
208
|
+
rows = []
|
209
|
+
for vm_id, stream_id in items.items():
|
210
|
+
try:
|
211
|
+
s = reader.get_stream(int(stream_id))
|
212
|
+
vested = max(min(now, int(s["stopTime"])) - int(s["startTime"]), 0) * int(s["ratePerSecond"]) # type: ignore
|
213
|
+
withdrawable = max(int(vested) - int(s["withdrawn"]), 0)
|
214
|
+
remaining = max(int(s["stopTime"]) - now, 0)
|
215
|
+
ok, reason = reader.verify_stream(int(stream_id), settings.PROVIDER_ID)
|
216
|
+
rows.append({
|
217
|
+
"vm_id": vm_id,
|
218
|
+
"stream_id": int(stream_id),
|
219
|
+
"recipient": s["recipient"],
|
220
|
+
"start": int(s["startTime"]),
|
221
|
+
"stop": int(s["stopTime"]),
|
222
|
+
"rate": int(s["ratePerSecond"]),
|
223
|
+
"deposit": int(s["deposit"]),
|
224
|
+
"withdrawn": int(s["withdrawn"]),
|
225
|
+
"remaining": remaining,
|
226
|
+
"verified": bool(ok),
|
227
|
+
"reason": reason,
|
228
|
+
"withdrawable": int(withdrawable),
|
229
|
+
})
|
230
|
+
except Exception as e:
|
231
|
+
rows.append({"vm_id": vm_id, "stream_id": int(stream_id), "error": str(e)})
|
232
|
+
if json_out:
|
233
|
+
print(_json.dumps({"streams": rows}, indent=2))
|
234
|
+
return
|
235
|
+
if not rows:
|
236
|
+
print("No streams mapped.")
|
237
|
+
return
|
238
|
+
print("\nStreams (VM → stream_id, remaining s, verified):")
|
239
|
+
for r in rows:
|
240
|
+
if "error" in r:
|
241
|
+
print(f"- {r['vm_id']}: {r['stream_id']} ERROR: {r['error']}")
|
242
|
+
else:
|
243
|
+
print(f"- {r['vm_id']}: {r['stream_id']} remaining={r['remaining']}s verified={r['verified']} reason={r['reason']} withdrawable={r['withdrawable']}")
|
244
|
+
except Exception as e:
|
245
|
+
print(f"Error: {e}")
|
246
|
+
raise typer.Exit(code=1)
|
247
|
+
|
248
|
+
|
249
|
+
@streams_app.command("show")
|
250
|
+
def streams_show(vm_id: str = typer.Argument(..., help="VM id (requestor_name)"), json_out: bool = typer.Option(False, "--json")):
|
251
|
+
"""Show a single VM's stream status."""
|
252
|
+
from .container import Container
|
253
|
+
from .config import settings
|
254
|
+
from .payments.blockchain_service import StreamPaymentReader
|
255
|
+
import json as _json
|
256
|
+
try:
|
257
|
+
c = Container()
|
258
|
+
c.config.from_pydantic(settings)
|
259
|
+
stream_map = c.stream_map()
|
260
|
+
sid = asyncio.run(stream_map.get(vm_id))
|
261
|
+
if sid is None:
|
262
|
+
print("No stream mapped for this VM.")
|
263
|
+
raise typer.Exit(code=1)
|
264
|
+
reader = StreamPaymentReader(settings.POLYGON_RPC_URL, settings.STREAM_PAYMENT_ADDRESS)
|
265
|
+
s = reader.get_stream(int(sid))
|
266
|
+
now = int(reader.web3.eth.get_block("latest")["timestamp"]) # type: ignore
|
267
|
+
vested = max(min(now, int(s["stopTime"])) - int(s["startTime"]), 0) * int(s["ratePerSecond"]) # type: ignore
|
268
|
+
withdrawable = max(int(vested) - int(s["withdrawn"]), 0)
|
269
|
+
remaining = max(int(s["stopTime"]) - now, 0)
|
270
|
+
ok, reason = reader.verify_stream(int(sid), settings.PROVIDER_ID)
|
271
|
+
out = {
|
272
|
+
"vm_id": vm_id,
|
273
|
+
"stream_id": int(sid),
|
274
|
+
"chain": s,
|
275
|
+
"computed": {
|
276
|
+
"now": now,
|
277
|
+
"remaining_seconds": remaining,
|
278
|
+
"vested_wei": int(vested),
|
279
|
+
"withdrawable_wei": int(withdrawable),
|
280
|
+
},
|
281
|
+
"verified": bool(ok),
|
282
|
+
"reason": reason,
|
283
|
+
}
|
284
|
+
if json_out:
|
285
|
+
print(_json.dumps(out, indent=2))
|
286
|
+
else:
|
287
|
+
print(f"VM {vm_id}: stream {sid} remaining={remaining}s verified={ok} withdrawable={withdrawable}")
|
288
|
+
except Exception as e:
|
289
|
+
print(f"Error: {e}")
|
290
|
+
raise typer.Exit(code=1)
|
291
|
+
|
188
292
|
@cli.command()
|
189
293
|
def start(
|
190
294
|
no_verify_port: bool = typer.Option(False, "--no-verify-port", help="Skip provider port verification."),
|
@@ -17,6 +17,10 @@ class StreamMonitor:
|
|
17
17
|
|
18
18
|
def start(self):
|
19
19
|
if self.settings.STREAM_MONITOR_ENABLED or self.settings.STREAM_WITHDRAW_ENABLED:
|
20
|
+
logger.info(
|
21
|
+
f"⏱️ Stream monitor enabled (check={self.settings.STREAM_MONITOR_ENABLED}, withdraw={self.settings.STREAM_WITHDRAW_ENABLED}) "
|
22
|
+
f"interval={self.settings.STREAM_MONITOR_INTERVAL_SECONDS}s"
|
23
|
+
)
|
20
24
|
self._task = asyncio.create_task(self._run(), name="stream-monitor")
|
21
25
|
|
22
26
|
async def stop(self):
|
@@ -34,6 +38,7 @@ class StreamMonitor:
|
|
34
38
|
await asyncio.sleep(self.settings.STREAM_MONITOR_INTERVAL_SECONDS)
|
35
39
|
items = await self.stream_map.all_items()
|
36
40
|
now = int(self.reader.web3.eth.get_block("latest")["timestamp"]) if items else 0
|
41
|
+
logger.debug(f"stream monitor tick: {len(items)} streams, now={now}")
|
37
42
|
for vm_id, stream_id in items.items():
|
38
43
|
try:
|
39
44
|
s = self.reader.get_stream(stream_id)
|
@@ -42,6 +47,10 @@ class StreamMonitor:
|
|
42
47
|
continue
|
43
48
|
# Stop VM if remaining runway < threshold
|
44
49
|
remaining = max(s["stopTime"] - now, 0)
|
50
|
+
logger.debug(
|
51
|
+
f"stream {stream_id} for VM {vm_id}: start={s['startTime']} stop={s['stopTime']} "
|
52
|
+
f"rate={s['ratePerSecond']} withdrawn={s['withdrawn']} halted={s['halted']} remaining={remaining}s"
|
53
|
+
)
|
45
54
|
if self.settings.STREAM_MONITOR_ENABLED and remaining < self.settings.STREAM_MIN_REMAINING_SECONDS:
|
46
55
|
logger.info(f"Stopping VM {vm_id} due to low stream runway ({remaining}s)")
|
47
56
|
try:
|
@@ -52,6 +61,7 @@ class StreamMonitor:
|
|
52
61
|
if self.settings.STREAM_WITHDRAW_ENABLED and self.client:
|
53
62
|
vested = max(min(now, s["stopTime"]) - s["startTime"], 0) * s["ratePerSecond"]
|
54
63
|
withdrawable = max(vested - s["withdrawn"], 0)
|
64
|
+
logger.debug(f"withdraw check stream {stream_id}: vested={vested} withdrawable={withdrawable}")
|
55
65
|
# Enforce a minimum interval between withdrawals
|
56
66
|
if withdrawable >= self.settings.STREAM_MIN_WITHDRAW_WEI and (
|
57
67
|
now - last_withdraw >= self.settings.STREAM_WITHDRAW_INTERVAL_SECONDS
|
@@ -1,6 +1,6 @@
|
|
1
1
|
[tool.poetry]
|
2
2
|
name = "golem-vm-provider"
|
3
|
-
version = "0.1.
|
3
|
+
version = "0.1.44"
|
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"
|
@@ -19,6 +19,11 @@ packages = [
|
|
19
19
|
{ include = "provider" }
|
20
20
|
]
|
21
21
|
|
22
|
+
# Include package data (deployment JSON)
|
23
|
+
include = [
|
24
|
+
{ path = "provider/data/deployments/l2.json", format = "wheel" },
|
25
|
+
]
|
26
|
+
|
22
27
|
[tool.poetry.scripts]
|
23
28
|
golem-provider = "provider.main:cli"
|
24
29
|
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{golem_vm_provider-0.1.43 → golem_vm_provider-0.1.44}/provider/discovery/golem_base_advertiser.py
RENAMED
File without changes
|
{golem_vm_provider-0.1.43 → golem_vm_provider-0.1.44}/provider/discovery/golem_base_utils.py
RENAMED
File without changes
|
{golem_vm_provider-0.1.43 → golem_vm_provider-0.1.44}/provider/discovery/multi_advertiser.py
RENAMED
File without changes
|
{golem_vm_provider-0.1.43 → golem_vm_provider-0.1.44}/provider/discovery/resource_monitor.py
RENAMED
File without changes
|
{golem_vm_provider-0.1.43 → golem_vm_provider-0.1.44}/provider/discovery/resource_tracker.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
{golem_vm_provider-0.1.43 → golem_vm_provider-0.1.44}/provider/payments/blockchain_service.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|