request-vm-on-golem 0.1.41__py3-none-any.whl → 0.1.45__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: request-vm-on-golem
3
- Version: 0.1.41
3
+ Version: 0.1.45
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 (Polygon GLM)
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 (approve + `createStream(GLM, provider_id, deposit, ratePerSecond)`), capture `stream_id`.
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` — Polygon PoS RPC URL
171
- - `stream_payment_address` — StreamPayment address
172
- - `glm_token_address` — GLM ERC20 address
173
- - `provider_eth_address` — optional helper for development; in production always use `/provider/info`
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=b9_Vy30rf3hpoBb_9Gu9z4mPWF3_oBjUjd-5pX2bVvU,33277
5
- requestor/config.py,sha256=suWN2-4AL0Pphu2t-_WiTKIFG-0_cJjWwR_CIOW1c24,5320
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=w9ONdKVf2E7BCEgDRiTeEqwdNTr8SqZiXHKdV5JH2ZQ,5757
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=auUc5XSHWbtOzyLHFqJ4RF337rCNYThzb7TjvUaK_uo,14542
16
+ requestor/services/provider_service.py,sha256=eb4t6tkcw9VzJev2sfawT1KvVc5TxQnb1pgYgoQZcM4,15000
16
17
  requestor/services/ssh_service.py,sha256=tcOCtk2SlB9Uuv-P2ghR22e7BJ9kigQh5b4zSGdPFns,4280
17
- requestor/services/vm_service.py,sha256=UPPj7aD0b7nCyNH6I-b35xdsqAkRUiQlpQBVGK4B7ms,8858
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.41.dist-info/METADATA,sha256=znwTKJHh21R9DsDgeHeaKFwxZFFC95vOTj-kScsc2Tc,11924
23
- request_vm_on_golem-0.1.41.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
24
- request_vm_on_golem-0.1.41.dist-info/entry_points.txt,sha256=Z-skRNpJ8aZcIl_En9mEm1ygkp9FKy0bzQoL3zO52-0,44
25
- request_vm_on_golem-0.1.41.dist-info/RECORD,,
23
+ request_vm_on_golem-0.1.45.dist-info/METADATA,sha256=Ln_GhY2wyZ0feb3EgI0x12zbnc1Zm0u6lyaNFRTF8LI,13912
24
+ request_vm_on_golem-0.1.45.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
25
+ request_vm_on_golem-0.1.45.dist-info/entry_points.txt,sha256=Z-skRNpJ8aZcIl_En9mEm1ygkp9FKy0bzQoL3zO52-0,44
26
+ request_vm_on_golem-0.1.45.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
- def cli():
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
- # Polygon / Payments
103
+ # Payments (EVM RPC)
98
104
  polygon_rpc_url: str = Field(
99
- default="https://polygon-rpc.com",
100
- description="Polygon PoS RPC URL for GLM payments"
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="GLM ERC20 token address on target network"
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.erc20 = self.web3.eth.contract(
86
- address=Web3.to_checksum_address(cfg.glm_token_address), abi=ERC20_ABI
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
- tx = fn.build_transaction(
91
- {
92
- "from": self.account.address,
93
- "nonce": self.web3.eth.get_transaction_count(self.account.address),
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
- # 1) Approve deposit for the StreamPayment contract
107
- approve = self.erc20.functions.approve(self.contract.address, int(deposit_wei))
108
- self._send(approve)
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
- # 2) Create stream
91
+ # Create stream
111
92
  fn = self.contract.functions.createStream(
112
- self.erc20.address,
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
- receipt = self._send(fn)
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 receipt["logs"]:
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
- # Approve first
143
- approve = self.erc20.functions.approve(self.contract.address, int(amount_wei))
144
- self._send(approve)
145
- # Top up
146
- fn = self.contract.functions.topUp(int(stream_id), int(amount_wei))
147
- receipt = self._send(fn)
148
- return receipt["transactionHash"]
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
+
@@ -92,7 +92,18 @@ class ProviderService:
92
92
  ) -> List[Dict]:
93
93
  """Find providers using Golem Base."""
94
94
  try:
95
+ def _to_float(val):
96
+ if val is None:
97
+ return None
98
+ try:
99
+ return float(val)
100
+ except Exception:
101
+ return None
102
+
95
103
  query = 'golem_type="provider"'
104
+ # Filter by advertised network to avoid cross-network results
105
+ if config.network:
106
+ query += f' && golem_network="{config.network}"'
96
107
  if cpu:
97
108
  query += f' && golem_cpu>={cpu}'
98
109
  if memory:
@@ -125,12 +136,12 @@ class ProviderService:
125
136
  'storage': int(annotations.get('golem_storage', 0)),
126
137
  },
127
138
  'pricing': {
128
- 'usd_per_core_month': annotations.get('golem_price_usd_core_month'),
129
- 'usd_per_gb_ram_month': annotations.get('golem_price_usd_ram_gb_month'),
130
- 'usd_per_gb_storage_month': annotations.get('golem_price_usd_storage_gb_month'),
131
- 'glm_per_core_month': annotations.get('golem_price_glm_core_month'),
132
- 'glm_per_gb_ram_month': annotations.get('golem_price_glm_ram_gb_month'),
133
- 'glm_per_gb_storage_month': annotations.get('golem_price_glm_storage_gb_month'),
139
+ 'usd_per_core_month': _to_float(annotations.get('golem_price_usd_core_month')),
140
+ 'usd_per_gb_ram_month': _to_float(annotations.get('golem_price_usd_ram_gb_month')),
141
+ 'usd_per_gb_storage_month': _to_float(annotations.get('golem_price_usd_storage_gb_month')),
142
+ 'glm_per_core_month': _to_float(annotations.get('golem_price_glm_core_month')),
143
+ 'glm_per_gb_ram_month': _to_float(annotations.get('golem_price_glm_ram_gb_month')),
144
+ 'glm_per_gb_storage_month': _to_float(annotations.get('golem_price_glm_storage_gb_month')),
134
145
  },
135
146
  'created_at_block': metadata.expires_at_block - (config.advertisement_interval * 2)
136
147
  }
@@ -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
- # Optionally create a GLM stream (if configured)
56
- from ..config import config as app_config
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 = {