request-vm-on-golem 0.1.41__py3-none-any.whl → 0.1.44__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.
- {request_vm_on_golem-0.1.41.dist-info → request_vm_on_golem-0.1.44.dist-info}/METADATA +50 -11
- {request_vm_on_golem-0.1.41.dist-info → request_vm_on_golem-0.1.44.dist-info}/RECORD +10 -9
- requestor/cli/commands.py +51 -7
- requestor/config.py +37 -4
- requestor/payments/blockchain_service.py +94 -81
- requestor/security/faucet.py +54 -0
- requestor/services/provider_service.py +3 -0
- requestor/services/vm_service.py +2 -21
- {request_vm_on_golem-0.1.41.dist-info → request_vm_on_golem-0.1.44.dist-info}/WHEEL +0 -0
- {request_vm_on_golem-0.1.41.dist-info → request_vm_on_golem-0.1.44.dist-info}/entry_points.txt +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.3
|
2
2
|
Name: request-vm-on-golem
|
3
|
-
Version: 0.1.
|
3
|
+
Version: 0.1.44
|
4
4
|
Summary: VM on Golem Requestor CLI - Create and manage virtual machines on the Golem Network
|
5
5
|
Keywords: golem,vm,cloud,decentralized,cli
|
6
6
|
Author: Phillip Jensen
|
@@ -25,6 +25,8 @@ Requires-Dist: colorlog (>=6.8.0,<7.0.0)
|
|
25
25
|
Requires-Dist: cryptography (>=3.4.7,<4.0.0)
|
26
26
|
Requires-Dist: fastapi (>=0.103.0,<0.104.0)
|
27
27
|
Requires-Dist: golem-base-sdk (==0.1.0)
|
28
|
+
Requires-Dist: golem-faucet (>=0.1.0,<0.2.0)
|
29
|
+
Requires-Dist: golem-streaming-abi (>=0.1.0,<0.2.0)
|
28
30
|
Requires-Dist: httptools (>=0.6.0,<0.7.0)
|
29
31
|
Requires-Dist: pydantic (>=2.4.0,<3.0.0)
|
30
32
|
Requires-Dist: pydantic-settings (>=2.1.0,<3.0.0)
|
@@ -119,17 +121,17 @@ The SSH connection process:
|
|
119
121
|
2. The provider's proxy system forwards your SSH connection to the VM
|
120
122
|
3. All traffic is securely routed through the allocated port
|
121
123
|
|
122
|
-
## Streaming Payments (
|
124
|
+
## Streaming Payments (Native ETH on L2)
|
123
125
|
|
124
|
-
This requestor integrates with an on‑chain StreamPayment contract to enable “pay‑as‑you‑go” rentals.
|
126
|
+
This requestor integrates with an on‑chain StreamPayment contract to enable “pay‑as‑you‑go” rentals using native ETH (no ERC20 approvals when the token address is zero).
|
125
127
|
|
126
128
|
Flow:
|
127
129
|
|
128
|
-
1. Fetch provider info:
|
129
|
-
- `GET http://{provider}:7466/api/v1/provider/info` → `provider_id`, `stream_payment_address`, `glm_token_address
|
130
|
+
1. Fetch provider info (preferred addresses):
|
131
|
+
- `GET http://{provider}:7466/api/v1/provider/info` → `provider_id`, `stream_payment_address`, `glm_token_address` (zero address means native ETH).
|
130
132
|
2. Compute `ratePerSecond` from provider pricing and requested VM resources.
|
131
133
|
3. Ensure `deposit >= ratePerSecond * 3600` (≥ 1 hour runway recommended/minimum).
|
132
|
-
4. Create a stream (
|
134
|
+
4. Create a stream (`createStream(0x000...0, provider_id, deposit, ratePerSecond)` plus `value=deposit`), capture `stream_id`. For ERC20 mode use a token address and approve first.
|
133
135
|
5. Create VM: `POST /api/v1/vms` with `stream_id` included.
|
134
136
|
6. Top‑up over time with `topUp(stream_id, amount)` to extend stopTime and keep the VM running indefinitely.
|
135
137
|
7. On stop/destroy: the requestor will best‑effort `withdraw` / `terminate` to settle.
|
@@ -156,7 +158,7 @@ poetry run golem vm stream topup --stream-id 123 --hours 3
|
|
156
158
|
poetry run golem vm stream topup --stream-id 123 --glm 25.0
|
157
159
|
```
|
158
160
|
|
159
|
-
- Create a VM and attach an existing stream:
|
161
|
+
- Create a VM and attach an existing stream (no auto-streams are created by the requestor):
|
160
162
|
|
161
163
|
```bash
|
162
164
|
poetry run golem vm create my-vm \
|
@@ -167,15 +169,30 @@ poetry run golem vm create my-vm \
|
|
167
169
|
|
168
170
|
Environment (env prefix `GOLEM_REQUESTOR_`):
|
169
171
|
|
170
|
-
- `polygon_rpc_url` —
|
171
|
-
- `stream_payment_address` — StreamPayment address
|
172
|
-
- `glm_token_address` —
|
173
|
-
- `provider_eth_address` — optional helper
|
172
|
+
- `polygon_rpc_url` — EVM RPC URL (default L2 RPC)
|
173
|
+
- `stream_payment_address` — StreamPayment address (fallback if provider doesn’t advertise)
|
174
|
+
- `glm_token_address` — Token address; set to zero address to use native ETH
|
175
|
+
- `provider_eth_address` — optional dev helper; in production always use `/provider/info`
|
176
|
+
- `network` — Target network for discovery filtering: `testnet` (default) or `mainnet`
|
174
177
|
|
175
178
|
Efficiency tips:
|
176
179
|
|
177
180
|
- Batch top‑ups (e.g., add several hours at once) to reduce on‑chain calls.
|
178
181
|
- Withdrawals are typically executed by providers; requestors don’t need to withdraw.
|
182
|
+
- The CLI `vm stream open` will prefer the provider’s advertised contract/token addresses to prevent mismatches.
|
183
|
+
|
184
|
+
## Faucet (L2 ETH)
|
185
|
+
|
186
|
+
- Request L2 test ETH to cover stream transactions:
|
187
|
+
|
188
|
+
```bash
|
189
|
+
poetry run golem wallet faucet
|
190
|
+
```
|
191
|
+
|
192
|
+
- Defaults:
|
193
|
+
- Faucet: `https://l2.holesky.golemdb.io/faucet`
|
194
|
+
- CAPTCHA: `https://cap.gobas.me/05381a2cef5e`
|
195
|
+
- Override with env: `GOLEM_REQUESTOR_l2_faucet_url`, `GOLEM_REQUESTOR_captcha_url`, `GOLEM_REQUESTOR_captcha_api_key`.
|
179
196
|
|
180
197
|
## Installation
|
181
198
|
|
@@ -217,6 +234,27 @@ Alternatively, you can prepend the environment variables directly to the command
|
|
217
234
|
GOLEM_REQUESTOR_ENVIRONMENT="development" GOLEM_REQUESTOR_FORCE_LOCALHOST="true" poetry run golem vm providers
|
218
235
|
```
|
219
236
|
|
237
|
+
### Mode vs. Network
|
238
|
+
|
239
|
+
- Development Mode (`GOLEM_REQUESTOR_ENVIRONMENT=development`)
|
240
|
+
- Improves local workflows: prefixes central discovery URL with `DEVMODE-` and, when using the central driver, maps provider IPs to `localhost` for easier testing.
|
241
|
+
- Does not determine chain selection.
|
242
|
+
|
243
|
+
- Network Selection (`--network` or `GOLEM_REQUESTOR_NETWORK`)
|
244
|
+
- Filters Golem Base discovery results by `golem_network=testnet|mainnet`.
|
245
|
+
- Combine with the appropriate RPC envs (`GOLEM_REQUESTOR_GOLEM_BASE_RPC_URL`, `GOLEM_REQUESTOR_GOLEM_BASE_WS_URL`) and any contract addresses.
|
246
|
+
- Independent from dev ergonomics.
|
247
|
+
|
248
|
+
Examples:
|
249
|
+
- List providers on mainnet without changing env:
|
250
|
+
```bash
|
251
|
+
poetry run golem vm providers --network mainnet
|
252
|
+
```
|
253
|
+
- Create a VM while targeting testnet:
|
254
|
+
```bash
|
255
|
+
poetry run golem vm create my-vm --provider-id 0xProvider --cpu 2 --memory 4 --storage 20 --network testnet
|
256
|
+
```
|
257
|
+
|
220
258
|
## Usage
|
221
259
|
|
222
260
|
### Provider Discovery
|
@@ -334,6 +372,7 @@ export GOLEM_REQUESTOR_DB_PATH="/path/to/database.db"
|
|
334
372
|
# Environment Mode (defaults to "production")
|
335
373
|
export GOLEM_REQUESTOR_ENVIRONMENT="development" # Optional: Switch to development mode
|
336
374
|
export GOLEM_REQUESTOR_FORCE_LOCALHOST="true" # Optional: Force localhost in development mode
|
375
|
+
export GOLEM_REQUESTOR_NETWORK="testnet" # Or "mainnet"; filters Golem Base results by annotation
|
337
376
|
```
|
338
377
|
|
339
378
|
2. Directory Structure:
|
@@ -1,25 +1,26 @@
|
|
1
1
|
requestor/__init__.py,sha256=OqSUAh1uZBMx7GW0MoSMg967PVdmT8XdPJx3QYjwkak,116
|
2
2
|
requestor/api/main.py,sha256=7utCzFNbh5Ol-vsBWeSwT4lXeHD7zdA-GFZuS3rHMWc,2180
|
3
3
|
requestor/cli/__init__.py,sha256=e3E4oEGxmGj-STPtFkQwg_qIWhR0JAiAQdw3G1hXciU,37
|
4
|
-
requestor/cli/commands.py,sha256=
|
5
|
-
requestor/config.py,sha256=
|
4
|
+
requestor/cli/commands.py,sha256=YiJvCWDp__c8OZ1YdrVc7gAYRUab5ygNGPCGxDmn3jE,35288
|
5
|
+
requestor/config.py,sha256=v6557eetioJmuOPWKV4iAl7DeqGjgQ0gegqc0zEzJZU,6396
|
6
6
|
requestor/db/__init__.py,sha256=Gm5DfWls6uvCZZ3HGGnyRHswbUQdeA5OGN8yPwH0hc8,88
|
7
7
|
requestor/db/sqlite.py,sha256=l5pWbx2qlHuar1N_a0B9tVnmumLJY1w5rp3yZ7jmsC0,4146
|
8
8
|
requestor/errors.py,sha256=wVpHBuYgQx5pTe_SamugfK-k768noikY1RxvPOjQGko,665
|
9
|
-
requestor/payments/blockchain_service.py,sha256=
|
9
|
+
requestor/payments/blockchain_service.py,sha256=EejW51A6Xqc3PKnKnzjRQ6pIVkH1FqacG4lwQAQ0HiM,6888
|
10
10
|
requestor/provider/__init__.py,sha256=fmW23aYUVciF8-gmBZkG-PLhn22upmcDzdPfAOLHG6g,103
|
11
11
|
requestor/provider/client.py,sha256=WXCm-1bytcgsuHEZzpg7RjjDOTuaXC9cj0Mrm7e6DSw,3676
|
12
12
|
requestor/run.py,sha256=GqOG6n34szt8Sp3SEqjRV6huWm737yCN6YnBqoiwI-U,1785
|
13
|
+
requestor/security/faucet.py,sha256=35d8mD3fM0YqRIhUXuIKandOL8vbw2T2IFQWVsan9Lw,2056
|
13
14
|
requestor/services/__init__.py,sha256=1qSn_6RMn0KB0A7LCnY2IW6_tC3HBQsdfkFeV-h94eM,172
|
14
15
|
requestor/services/database_service.py,sha256=GlSrzzzd7PSYQJNup00sxkB-B2PMr1__04K8k5QSWvs,2996
|
15
|
-
requestor/services/provider_service.py,sha256=
|
16
|
+
requestor/services/provider_service.py,sha256=SH76qxnbIm4EgCwQ0SHiwCidN19aTQMCxGUj1e0yGRQ,14712
|
16
17
|
requestor/services/ssh_service.py,sha256=tcOCtk2SlB9Uuv-P2ghR22e7BJ9kigQh5b4zSGdPFns,4280
|
17
|
-
requestor/services/vm_service.py,sha256=
|
18
|
+
requestor/services/vm_service.py,sha256=eQ2pPMpYlfPVbVFrkFElsRO5swPq-2XZEfuvxagyHDk,7941
|
18
19
|
requestor/ssh/__init__.py,sha256=hNgSqJ5s1_AwwxVRyFjUqh_LTBpI4Hmzq0F-f_wXN9g,119
|
19
20
|
requestor/ssh/manager.py,sha256=3jQtbbK7CVC2yD1zCO88jGXh2fBcuv3CzWEqDLuaQVk,9758
|
20
21
|
requestor/utils/logging.py,sha256=oFNpO8pJboYM8Wp7g3HOU4HFyBTKypVdY15lUiz1a4I,3721
|
21
22
|
requestor/utils/spinner.py,sha256=PUHJdTD9jpUHur__01_qxXy87WFfNmjQbD_sLG-KlGo,2459
|
22
|
-
request_vm_on_golem-0.1.
|
23
|
-
request_vm_on_golem-0.1.
|
24
|
-
request_vm_on_golem-0.1.
|
25
|
-
request_vm_on_golem-0.1.
|
23
|
+
request_vm_on_golem-0.1.44.dist-info/METADATA,sha256=oNqjV3CuQMnhZA1SRGPBf9M8J_-J8WsVBaSOdJnrxKE,13912
|
24
|
+
request_vm_on_golem-0.1.44.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
25
|
+
request_vm_on_golem-0.1.44.dist-info/entry_points.txt,sha256=Z-skRNpJ8aZcIl_En9mEm1ygkp9FKy0bzQoL3zO52-0,44
|
26
|
+
request_vm_on_golem-0.1.44.dist-info/RECORD,,
|
requestor/cli/commands.py
CHANGED
@@ -75,9 +75,17 @@ def print_version(ctx, param, value):
|
|
75
75
|
@click.group()
|
76
76
|
@click.option('--version', is_flag=True, callback=print_version,
|
77
77
|
expose_value=False, is_eager=True, help="Show the version and exit.")
|
78
|
-
|
78
|
+
@click.option('--network', type=click.Choice(['testnet', 'mainnet']), default=None,
|
79
|
+
help="Override network for discovery filtering ('testnet' or 'mainnet')")
|
80
|
+
def cli(network: str | None):
|
79
81
|
"""VM on Golem management CLI"""
|
80
82
|
ensure_config()
|
83
|
+
# Allow on-demand override without touching env
|
84
|
+
if network:
|
85
|
+
try:
|
86
|
+
config.network = network
|
87
|
+
except Exception:
|
88
|
+
pass
|
81
89
|
pass
|
82
90
|
|
83
91
|
|
@@ -94,10 +102,14 @@ def vm():
|
|
94
102
|
@click.option('--country', help='Preferred provider country')
|
95
103
|
@click.option('--driver', type=click.Choice(['central', 'golem-base']), default=None, help='Discovery driver to use')
|
96
104
|
@click.option('--json', 'as_json', is_flag=True, help='Output in JSON format')
|
105
|
+
@click.option('--network', type=click.Choice(['testnet', 'mainnet']), default=None,
|
106
|
+
help='Override network filter for this command')
|
97
107
|
@async_command
|
98
|
-
async def list_providers(cpu: Optional[int], memory: Optional[int], storage: Optional[int], country: Optional[str], driver: Optional[str], as_json: bool):
|
108
|
+
async def list_providers(cpu: Optional[int], memory: Optional[int], storage: Optional[int], country: Optional[str], driver: Optional[str], as_json: bool, network: Optional[str] = None):
|
99
109
|
"""List available providers matching requirements."""
|
100
110
|
try:
|
111
|
+
if network:
|
112
|
+
config.network = network
|
101
113
|
# Log search criteria if any
|
102
114
|
if any([cpu, memory, storage, country]):
|
103
115
|
logger.command("🔍 Searching for providers with criteria:")
|
@@ -112,7 +124,7 @@ async def list_providers(cpu: Optional[int], memory: Optional[int], storage: Opt
|
|
112
124
|
|
113
125
|
# Determine the discovery driver being used
|
114
126
|
discovery_driver = driver or config.discovery_driver
|
115
|
-
logger.process(f"Querying discovery service via {discovery_driver}")
|
127
|
+
logger.process(f"Querying discovery service via {discovery_driver} (network={config.network})")
|
116
128
|
|
117
129
|
# Initialize provider service
|
118
130
|
provider_service = ProviderService()
|
@@ -175,10 +187,13 @@ async def list_providers(cpu: Optional[int], memory: Optional[int], storage: Opt
|
|
175
187
|
@click.option('--storage', type=int, required=True, help='Disk in GB')
|
176
188
|
@click.option('--stream-id', type=int, default=None, help='Optional StreamPayment stream id to fund this VM')
|
177
189
|
@click.option('--yes', is_flag=True, help='Do not prompt for confirmation')
|
190
|
+
@click.option('--network', type=click.Choice(['testnet', 'mainnet']), default=None, help='Override network for discovery during creation')
|
178
191
|
@async_command
|
179
|
-
async def create_vm(name: str, provider_id: str, cpu: int, memory: int, storage: int, stream_id: int | None, yes: bool):
|
192
|
+
async def create_vm(name: str, provider_id: str, cpu: int, memory: int, storage: int, stream_id: int | None, yes: bool, network: Optional[str] = None):
|
180
193
|
"""Create a new VM on a specific provider."""
|
181
194
|
try:
|
195
|
+
if network:
|
196
|
+
config.network = network
|
182
197
|
# Show configuration details
|
183
198
|
click.echo("\n" + "─" * 60)
|
184
199
|
click.echo(click.style(" VM Configuration", fg="blue", bold=True))
|
@@ -295,11 +310,14 @@ def vm_stream():
|
|
295
310
|
@click.option('--memory', type=int, required=True, help='Memory (GB) for rate calc')
|
296
311
|
@click.option('--storage', type=int, required=True, help='Storage (GB) for rate calc')
|
297
312
|
@click.option('--hours', type=int, default=1, help='Deposit coverage in hours (default 1)')
|
313
|
+
@click.option('--network', type=click.Choice(['testnet', 'mainnet']), default=None, help='Override network for discovery during stream open')
|
298
314
|
@async_command
|
299
|
-
async def stream_open(provider_id: str, cpu: int, memory: int, storage: int, hours: int):
|
315
|
+
async def stream_open(provider_id: str, cpu: int, memory: int, storage: int, hours: int, network: Optional[str] = None):
|
300
316
|
"""Create a GLM stream for a planned VM rental."""
|
301
317
|
from ..payments.blockchain_service import StreamPaymentClient, StreamPaymentConfig
|
302
318
|
try:
|
319
|
+
if network:
|
320
|
+
config.network = network
|
303
321
|
provider_service = ProviderService()
|
304
322
|
async with provider_service:
|
305
323
|
provider = await provider_service.verify_provider(provider_id)
|
@@ -319,10 +337,11 @@ async def stream_open(provider_id: str, cpu: int, memory: int, storage: int, hou
|
|
319
337
|
recipient = info['provider_id']
|
320
338
|
|
321
339
|
deposit_wei = rate_per_second_wei * int(hours) * 3600
|
340
|
+
# Prefer provider-advertised contract addresses to avoid mismatches
|
322
341
|
spc = StreamPaymentConfig(
|
323
342
|
rpc_url=config.polygon_rpc_url,
|
324
|
-
contract_address=config.stream_payment_address,
|
325
|
-
glm_token_address=config.glm_token_address,
|
343
|
+
contract_address=info.get('stream_payment_address') or config.stream_payment_address,
|
344
|
+
glm_token_address=info.get('glm_token_address') or config.glm_token_address,
|
326
345
|
private_key=config.ethereum_private_key,
|
327
346
|
)
|
328
347
|
sp = StreamPaymentClient(spc)
|
@@ -365,6 +384,31 @@ async def stream_topup(stream_id: int, glm: float | None, hours: int | None):
|
|
365
384
|
raise click.Abort()
|
366
385
|
|
367
386
|
|
387
|
+
@cli.group()
|
388
|
+
def wallet():
|
389
|
+
"""Wallet utilities (funding, balance)."""
|
390
|
+
pass
|
391
|
+
|
392
|
+
|
393
|
+
@wallet.command('faucet')
|
394
|
+
@async_command
|
395
|
+
async def wallet_faucet():
|
396
|
+
"""Request L2 faucet funds for the requestor's payment address."""
|
397
|
+
try:
|
398
|
+
from ..security.faucet import L2FaucetService
|
399
|
+
from eth_account import Account
|
400
|
+
acct = Account.from_key(config.ethereum_private_key)
|
401
|
+
service = L2FaucetService(config)
|
402
|
+
tx = await service.request_funds(acct.address)
|
403
|
+
if tx:
|
404
|
+
click.echo(json.dumps({"address": acct.address, "tx": tx}, indent=2))
|
405
|
+
else:
|
406
|
+
click.echo(json.dumps({"address": acct.address, "tx": None}, indent=2))
|
407
|
+
except Exception as e:
|
408
|
+
logger.error(f"Faucet request failed: {e}")
|
409
|
+
raise click.Abort()
|
410
|
+
|
411
|
+
|
368
412
|
@vm.command(name='ssh')
|
369
413
|
@click.argument('name')
|
370
414
|
@async_command
|
requestor/config.py
CHANGED
@@ -3,6 +3,7 @@ from typing import Optional, Dict
|
|
3
3
|
import os
|
4
4
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
5
5
|
from pydantic import Field, field_validator, ValidationInfo
|
6
|
+
import os
|
6
7
|
|
7
8
|
|
8
9
|
def ensure_config() -> None:
|
@@ -47,6 +48,11 @@ class RequestorConfig(BaseSettings):
|
|
47
48
|
default="production",
|
48
49
|
description="Environment mode: 'development' or 'production'"
|
49
50
|
)
|
51
|
+
# Network (for discovery filtering and defaults)
|
52
|
+
network: str = Field(
|
53
|
+
default="mainnet",
|
54
|
+
description="Target network: 'testnet' or 'mainnet'"
|
55
|
+
)
|
50
56
|
|
51
57
|
# Development Settings
|
52
58
|
force_localhost: bool = Field(
|
@@ -94,10 +100,10 @@ class RequestorConfig(BaseSettings):
|
|
94
100
|
description="Private key for Golem Base"
|
95
101
|
)
|
96
102
|
|
97
|
-
#
|
103
|
+
# Payments (EVM RPC)
|
98
104
|
polygon_rpc_url: str = Field(
|
99
|
-
default="https://
|
100
|
-
description="
|
105
|
+
default="https://l2.holesky.golemdb.io/rpc",
|
106
|
+
description="EVM RPC URL for streaming payments (L2 by default)"
|
101
107
|
)
|
102
108
|
stream_payment_address: str = Field(
|
103
109
|
default="0x0000000000000000000000000000000000000000",
|
@@ -105,13 +111,40 @@ class RequestorConfig(BaseSettings):
|
|
105
111
|
)
|
106
112
|
glm_token_address: str = Field(
|
107
113
|
default="0x0000000000000000000000000000000000000000",
|
108
|
-
description="
|
114
|
+
description="Token address (0x0 means native ETH)"
|
115
|
+
)
|
116
|
+
# Faucet settings (L2 payments)
|
117
|
+
l2_faucet_url: str = Field(
|
118
|
+
default="https://l2.holesky.golemdb.io/faucet",
|
119
|
+
description="L2 faucet base URL (no trailing /api)"
|
120
|
+
)
|
121
|
+
captcha_url: str = Field(
|
122
|
+
default="https://cap.gobas.me",
|
123
|
+
description="CAPTCHA base URL"
|
124
|
+
)
|
125
|
+
captcha_api_key: str = Field(
|
126
|
+
default="05381a2cef5e",
|
127
|
+
description="CAPTCHA API key path segment"
|
109
128
|
)
|
110
129
|
provider_eth_address: str = Field(
|
111
130
|
default="",
|
112
131
|
description="Optional provider Ethereum address for test/dev streaming"
|
113
132
|
)
|
114
133
|
|
134
|
+
@field_validator("polygon_rpc_url", mode='before')
|
135
|
+
@classmethod
|
136
|
+
def prefer_alt_env(cls, v: str) -> str:
|
137
|
+
# Accept alt aliases
|
138
|
+
for key in (
|
139
|
+
"GOLEM_REQUESTOR_l2_rpc_url",
|
140
|
+
"GOLEM_REQUESTOR_L2_RPC_URL",
|
141
|
+
"GOLEM_REQUESTOR_kaolin_rpc_url",
|
142
|
+
"GOLEM_REQUESTOR_KAOLIN_RPC_URL",
|
143
|
+
):
|
144
|
+
if os.environ.get(key):
|
145
|
+
return os.environ[key]
|
146
|
+
return v
|
147
|
+
|
115
148
|
# Base Directory
|
116
149
|
base_dir: Path = Field(
|
117
150
|
default_factory=lambda: Path.home() / ".golem" / "requestor",
|
@@ -5,64 +5,7 @@ from typing import Optional, Any, Dict
|
|
5
5
|
|
6
6
|
from web3 import Web3
|
7
7
|
from eth_account import Account
|
8
|
-
|
9
|
-
|
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
|
-
"anonymous": False,
|
39
|
-
"inputs": [
|
40
|
-
{"indexed": True, "internalType": "uint256", "name": "streamId", "type": "uint256"},
|
41
|
-
{"indexed": True, "internalType": "address", "name": "sender", "type": "address"},
|
42
|
-
{"indexed": True, "internalType": "address", "name": "recipient", "type": "address"},
|
43
|
-
{"indexed": False, "internalType": "address", "name": "token", "type": "address"},
|
44
|
-
{"indexed": False, "internalType": "uint256", "name": "deposit", "type": "uint256"},
|
45
|
-
{"indexed": False, "internalType": "uint256", "name": "ratePerSecond", "type": "uint256"},
|
46
|
-
{"indexed": False, "internalType": "uint256", "name": "startTime", "type": "uint256"},
|
47
|
-
{"indexed": False, "internalType": "uint256", "name": "stopTime", "type": "uint256"},
|
48
|
-
],
|
49
|
-
"name": "StreamCreated",
|
50
|
-
"type": "event",
|
51
|
-
},
|
52
|
-
]
|
53
|
-
|
54
|
-
ERC20_ABI = [
|
55
|
-
{
|
56
|
-
"name": "approve",
|
57
|
-
"type": "function",
|
58
|
-
"stateMutability": "nonpayable",
|
59
|
-
"inputs": [
|
60
|
-
{"name": "spender", "type": "address"},
|
61
|
-
{"name": "amount", "type": "uint256"},
|
62
|
-
],
|
63
|
-
"outputs": [{"name": "", "type": "bool"}],
|
64
|
-
}
|
65
|
-
]
|
8
|
+
from golem_streaming_abi import STREAM_PAYMENT_ABI, ERC20_ABI
|
66
9
|
|
67
10
|
|
68
11
|
@dataclass
|
@@ -82,17 +25,44 @@ class StreamPaymentClient:
|
|
82
25
|
self.contract = self.web3.eth.contract(
|
83
26
|
address=Web3.to_checksum_address(cfg.contract_address), abi=STREAM_PAYMENT_ABI
|
84
27
|
)
|
85
|
-
self.
|
86
|
-
|
87
|
-
|
28
|
+
self.token_address = Web3.to_checksum_address(cfg.glm_token_address)
|
29
|
+
self.native_eth = self.token_address.lower() == Web3.to_checksum_address("0x0000000000000000000000000000000000000000").lower()
|
30
|
+
self.erc20 = None
|
31
|
+
if not self.native_eth:
|
32
|
+
self.erc20 = self.web3.eth.contract(
|
33
|
+
address=self.token_address, abi=ERC20_ABI
|
34
|
+
)
|
88
35
|
|
89
36
|
def _send(self, fn) -> Dict[str, Any]:
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
37
|
+
base = {
|
38
|
+
"from": self.account.address,
|
39
|
+
"nonce": self.web3.eth.get_transaction_count(self.account.address),
|
40
|
+
}
|
41
|
+
# Fill chainId
|
42
|
+
try:
|
43
|
+
base["chainId"] = getattr(self.web3.eth, "chain_id", None) or self.web3.eth.chain_id
|
44
|
+
except Exception:
|
45
|
+
pass
|
46
|
+
# Try gas estimation and fee fields
|
47
|
+
try:
|
48
|
+
tx_preview = fn.build_transaction(base)
|
49
|
+
gas = self.web3.eth.estimate_gas(tx_preview)
|
50
|
+
base["gas"] = gas
|
51
|
+
except Exception:
|
52
|
+
pass
|
53
|
+
try:
|
54
|
+
# Prefer EIP-1559 if available
|
55
|
+
max_fee = getattr(self.web3.eth, "max_priority_fee", None)
|
56
|
+
if max_fee is not None:
|
57
|
+
base.setdefault("maxPriorityFeePerGas", max_fee)
|
58
|
+
base.setdefault("maxFeePerGas", getattr(self.web3.eth, "gas_price", lambda: None)() or self.web3.eth.gas_price)
|
59
|
+
except Exception:
|
60
|
+
try:
|
61
|
+
base.setdefault("gasPrice", self.web3.eth.gas_price)
|
62
|
+
except Exception:
|
63
|
+
pass
|
64
|
+
|
65
|
+
tx = fn.build_transaction(base)
|
96
66
|
# In production, sign and send raw; in tests, Account may be a dummy without signer
|
97
67
|
if hasattr(self.account, "sign_transaction"):
|
98
68
|
signed = self.account.sign_transaction(tx)
|
@@ -103,22 +73,47 @@ class StreamPaymentClient:
|
|
103
73
|
return {"transactionHash": tx_hash.hex(), "status": receipt.status, "logs": receipt.logs}
|
104
74
|
|
105
75
|
def create_stream(self, provider_address: str, deposit_wei: int, rate_per_second_wei: int) -> int:
|
106
|
-
|
107
|
-
|
108
|
-
|
76
|
+
tx_value = None
|
77
|
+
token_param = self.token_address if not self.native_eth else Web3.to_checksum_address("0x0000000000000000000000000000000000000000")
|
78
|
+
|
79
|
+
if not self.native_eth:
|
80
|
+
# Approve deposit for the StreamPayment contract (only if needed)
|
81
|
+
try:
|
82
|
+
allowance = self.erc20.functions.allowance(self.account.address, self.contract.address).call()
|
83
|
+
except Exception:
|
84
|
+
allowance = 0
|
85
|
+
if int(allowance) < int(deposit_wei):
|
86
|
+
approve = self.erc20.functions.approve(self.contract.address, int(deposit_wei))
|
87
|
+
self._send(approve)
|
88
|
+
else:
|
89
|
+
tx_value = int(deposit_wei)
|
109
90
|
|
110
|
-
#
|
91
|
+
# Create stream
|
111
92
|
fn = self.contract.functions.createStream(
|
112
|
-
|
93
|
+
token_param,
|
113
94
|
Web3.to_checksum_address(provider_address),
|
114
95
|
int(deposit_wei),
|
115
96
|
int(rate_per_second_wei),
|
116
97
|
)
|
117
|
-
|
98
|
+
# Include ETH value if native
|
99
|
+
if tx_value is not None:
|
100
|
+
# Build/send with value field
|
101
|
+
base = {
|
102
|
+
"from": self.account.address,
|
103
|
+
"nonce": self.web3.eth.get_transaction_count(self.account.address),
|
104
|
+
"value": tx_value,
|
105
|
+
}
|
106
|
+
tx = fn.build_transaction(base)
|
107
|
+
signed = self.account.sign_transaction(tx)
|
108
|
+
tx_hash = self.web3.eth.send_raw_transaction(signed.rawTransaction)
|
109
|
+
receipt = self.web3.eth.wait_for_transaction_receipt(tx_hash)
|
110
|
+
tx_receipt = {"transactionHash": tx_hash.hex(), "status": receipt.status, "logs": receipt.logs}
|
111
|
+
else:
|
112
|
+
tx_receipt = self._send(fn)
|
118
113
|
|
119
114
|
# Try to parse StreamCreated event for streamId
|
120
115
|
try:
|
121
|
-
for log in
|
116
|
+
for log in tx_receipt["logs"]:
|
122
117
|
# very naive filter: topic0 = keccak256(StreamCreated(...))
|
123
118
|
# When ABI is attached to contract, use contract.events
|
124
119
|
ev = self.contract.events.StreamCreated().process_log(log)
|
@@ -139,10 +134,28 @@ class StreamPaymentClient:
|
|
139
134
|
return receipt["transactionHash"]
|
140
135
|
|
141
136
|
def top_up(self, stream_id: int, amount_wei: int) -> str:
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
137
|
+
if not self.native_eth:
|
138
|
+
# Approve first (only if needed)
|
139
|
+
try:
|
140
|
+
allowance = self.erc20.functions.allowance(self.account.address, self.contract.address).call()
|
141
|
+
except Exception:
|
142
|
+
allowance = 0
|
143
|
+
if int(allowance) < int(amount_wei):
|
144
|
+
approve = self.erc20.functions.approve(self.contract.address, int(amount_wei))
|
145
|
+
self._send(approve)
|
146
|
+
fn = self.contract.functions.topUp(int(stream_id), int(amount_wei))
|
147
|
+
receipt = self._send(fn)
|
148
|
+
return receipt["transactionHash"]
|
149
|
+
else:
|
150
|
+
# Native ETH mode: send value along with call
|
151
|
+
fn = self.contract.functions.topUp(int(stream_id), int(amount_wei))
|
152
|
+
base = {
|
153
|
+
"from": self.account.address,
|
154
|
+
"nonce": self.web3.eth.get_transaction_count(self.account.address),
|
155
|
+
"value": int(amount_wei),
|
156
|
+
}
|
157
|
+
tx = fn.build_transaction(base)
|
158
|
+
signed = self.account.sign_transaction(tx)
|
159
|
+
tx_hash = self.web3.eth.send_raw_transaction(signed.rawTransaction)
|
160
|
+
self.web3.eth.wait_for_transaction_receipt(tx_hash)
|
161
|
+
return tx_hash.hex()
|
@@ -0,0 +1,54 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from typing import Optional, List, Tuple
|
4
|
+
|
5
|
+
from web3 import Web3
|
6
|
+
from golem_faucet import PowFaucetClient
|
7
|
+
from ..utils.logging import setup_logger
|
8
|
+
from ..config import RequestorConfig
|
9
|
+
|
10
|
+
logger = setup_logger(__name__)
|
11
|
+
|
12
|
+
|
13
|
+
class L2FaucetService:
|
14
|
+
def __init__(self, config: RequestorConfig):
|
15
|
+
self.cfg = config
|
16
|
+
self.web3 = Web3(Web3.HTTPProvider(config.polygon_rpc_url))
|
17
|
+
self.client = PowFaucetClient(
|
18
|
+
faucet_url=getattr(config, 'l2_faucet_url', 'https://l2.holesky.golemdb.io/faucet'),
|
19
|
+
captcha_base_url=getattr(config, 'captcha_url', 'https://cap.gobas.me'),
|
20
|
+
captcha_api_key=getattr(config, 'captcha_api_key', '05381a2cef5e'),
|
21
|
+
)
|
22
|
+
|
23
|
+
def _balance_eth(self, address: str) -> float:
|
24
|
+
try:
|
25
|
+
wei = self.web3.eth.get_balance(Web3.to_checksum_address(address))
|
26
|
+
return float(self.web3.from_wei(wei, 'ether'))
|
27
|
+
except Exception as e:
|
28
|
+
logger.warning(f"balance check failed: {e}")
|
29
|
+
return 0.0
|
30
|
+
|
31
|
+
async def request_funds(self, address: str) -> Optional[str]:
|
32
|
+
bal = self._balance_eth(address)
|
33
|
+
if bal > 0.01:
|
34
|
+
logger.info(f"Sufficient L2 funds ({bal} ETH), skipping faucet.")
|
35
|
+
return None
|
36
|
+
chall = await self.client.get_challenge()
|
37
|
+
if not chall:
|
38
|
+
logger.error("could not fetch faucet challenge")
|
39
|
+
return None
|
40
|
+
token = chall.get('token')
|
41
|
+
challenge_list = chall.get('challenge') or []
|
42
|
+
solutions: List[Tuple[str, str, int]] = []
|
43
|
+
for salt, target in challenge_list:
|
44
|
+
nonce = PowFaucetClient.solve_challenge(salt, target)
|
45
|
+
solutions.append((salt, target, nonce))
|
46
|
+
redeemed = await self.client.redeem(token, solutions)
|
47
|
+
if not redeemed:
|
48
|
+
logger.error("failed to redeem challenge")
|
49
|
+
return None
|
50
|
+
tx = await self.client.request_funds(address, redeemed)
|
51
|
+
if tx:
|
52
|
+
logger.success(f"L2 faucet sent tx: {tx}")
|
53
|
+
return tx
|
54
|
+
|
@@ -93,6 +93,9 @@ class ProviderService:
|
|
93
93
|
"""Find providers using Golem Base."""
|
94
94
|
try:
|
95
95
|
query = 'golem_type="provider"'
|
96
|
+
# Filter by advertised network to avoid cross-network results
|
97
|
+
if config.network:
|
98
|
+
query += f' && golem_network="{config.network}"'
|
96
99
|
if cpu:
|
97
100
|
query += f' && golem_cpu>={cpu}'
|
98
101
|
if memory:
|
requestor/services/vm_service.py
CHANGED
@@ -52,27 +52,8 @@ class VMService:
|
|
52
52
|
# Get VM access info
|
53
53
|
access_info = await self.provider_client.get_vm_access(vm['id'])
|
54
54
|
|
55
|
-
#
|
56
|
-
|
57
|
-
stream_id = None
|
58
|
-
try:
|
59
|
-
if (
|
60
|
-
self.blockchain_client
|
61
|
-
and app_config.stream_payment_address != "0x0000000000000000000000000000000000000000"
|
62
|
-
and app_config.glm_token_address != "0x0000000000000000000000000000000000000000"
|
63
|
-
and app_config.provider_eth_address
|
64
|
-
):
|
65
|
-
# Simple heuristic: deposit for 1 hour at a nominal rate
|
66
|
-
rate_per_second = 10**18 # 1 GLM / second (example)
|
67
|
-
deposit = rate_per_second * 3600 # 1 hour worth
|
68
|
-
stream_id = self.blockchain_client.create_stream(
|
69
|
-
app_config.provider_eth_address,
|
70
|
-
deposit,
|
71
|
-
rate_per_second,
|
72
|
-
)
|
73
|
-
except Exception:
|
74
|
-
# Do not fail VM creation if streaming setup fails
|
75
|
-
stream_id = None
|
55
|
+
# Preserve any provided stream_id; do not auto-create streams here
|
56
|
+
# Stream creation should be explicit via CLI `vm stream open` command.
|
76
57
|
|
77
58
|
# Save VM details to database
|
78
59
|
config = {
|
File without changes
|
{request_vm_on_golem-0.1.41.dist-info → request_vm_on_golem-0.1.44.dist-info}/entry_points.txt
RENAMED
File without changes
|