request-vm-on-golem 0.1.41__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.
Files changed (26) hide show
  1. {request_vm_on_golem-0.1.41 → request_vm_on_golem-0.1.44}/PKG-INFO +50 -11
  2. {request_vm_on_golem-0.1.41 → request_vm_on_golem-0.1.44}/README.md +47 -10
  3. {request_vm_on_golem-0.1.41 → request_vm_on_golem-0.1.44}/pyproject.toml +5 -3
  4. {request_vm_on_golem-0.1.41 → request_vm_on_golem-0.1.44}/requestor/cli/commands.py +51 -7
  5. {request_vm_on_golem-0.1.41 → request_vm_on_golem-0.1.44}/requestor/config.py +37 -4
  6. request_vm_on_golem-0.1.44/requestor/payments/blockchain_service.py +161 -0
  7. request_vm_on_golem-0.1.44/requestor/security/faucet.py +54 -0
  8. {request_vm_on_golem-0.1.41 → request_vm_on_golem-0.1.44}/requestor/services/provider_service.py +3 -0
  9. {request_vm_on_golem-0.1.41 → request_vm_on_golem-0.1.44}/requestor/services/vm_service.py +2 -21
  10. request_vm_on_golem-0.1.41/requestor/payments/blockchain_service.py +0 -148
  11. {request_vm_on_golem-0.1.41 → request_vm_on_golem-0.1.44}/requestor/__init__.py +0 -0
  12. {request_vm_on_golem-0.1.41 → request_vm_on_golem-0.1.44}/requestor/api/main.py +0 -0
  13. {request_vm_on_golem-0.1.41 → request_vm_on_golem-0.1.44}/requestor/cli/__init__.py +0 -0
  14. {request_vm_on_golem-0.1.41 → request_vm_on_golem-0.1.44}/requestor/db/__init__.py +0 -0
  15. {request_vm_on_golem-0.1.41 → request_vm_on_golem-0.1.44}/requestor/db/sqlite.py +0 -0
  16. {request_vm_on_golem-0.1.41 → request_vm_on_golem-0.1.44}/requestor/errors.py +0 -0
  17. {request_vm_on_golem-0.1.41 → request_vm_on_golem-0.1.44}/requestor/provider/__init__.py +0 -0
  18. {request_vm_on_golem-0.1.41 → request_vm_on_golem-0.1.44}/requestor/provider/client.py +0 -0
  19. {request_vm_on_golem-0.1.41 → request_vm_on_golem-0.1.44}/requestor/run.py +0 -0
  20. {request_vm_on_golem-0.1.41 → request_vm_on_golem-0.1.44}/requestor/services/__init__.py +0 -0
  21. {request_vm_on_golem-0.1.41 → request_vm_on_golem-0.1.44}/requestor/services/database_service.py +0 -0
  22. {request_vm_on_golem-0.1.41 → request_vm_on_golem-0.1.44}/requestor/services/ssh_service.py +0 -0
  23. {request_vm_on_golem-0.1.41 → request_vm_on_golem-0.1.44}/requestor/ssh/__init__.py +0 -0
  24. {request_vm_on_golem-0.1.41 → request_vm_on_golem-0.1.44}/requestor/ssh/manager.py +0 -0
  25. {request_vm_on_golem-0.1.41 → request_vm_on_golem-0.1.44}/requestor/utils/logging.py +0 -0
  26. {request_vm_on_golem-0.1.41 → request_vm_on_golem-0.1.44}/requestor/utils/spinner.py +0 -0
@@ -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.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 (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:
@@ -80,17 +80,17 @@ The SSH connection process:
80
80
  2. The provider's proxy system forwards your SSH connection to the VM
81
81
  3. All traffic is securely routed through the allocated port
82
82
 
83
- ## Streaming Payments (Polygon GLM)
83
+ ## Streaming Payments (Native ETH on L2)
84
84
 
85
- This requestor integrates with an on‑chain StreamPayment contract to enable “pay‑as‑you‑go” rentals.
85
+ 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).
86
86
 
87
87
  Flow:
88
88
 
89
- 1. Fetch provider info:
90
- - `GET http://{provider}:7466/api/v1/provider/info` → `provider_id`, `stream_payment_address`, `glm_token_address`.
89
+ 1. Fetch provider info (preferred addresses):
90
+ - `GET http://{provider}:7466/api/v1/provider/info` → `provider_id`, `stream_payment_address`, `glm_token_address` (zero address means native ETH).
91
91
  2. Compute `ratePerSecond` from provider pricing and requested VM resources.
92
92
  3. Ensure `deposit >= ratePerSecond * 3600` (≥ 1 hour runway recommended/minimum).
93
- 4. Create a stream (approve + `createStream(GLM, provider_id, deposit, ratePerSecond)`), capture `stream_id`.
93
+ 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.
94
94
  5. Create VM: `POST /api/v1/vms` with `stream_id` included.
95
95
  6. Top‑up over time with `topUp(stream_id, amount)` to extend stopTime and keep the VM running indefinitely.
96
96
  7. On stop/destroy: the requestor will best‑effort `withdraw` / `terminate` to settle.
@@ -117,7 +117,7 @@ poetry run golem vm stream topup --stream-id 123 --hours 3
117
117
  poetry run golem vm stream topup --stream-id 123 --glm 25.0
118
118
  ```
119
119
 
120
- - Create a VM and attach an existing stream:
120
+ - Create a VM and attach an existing stream (no auto-streams are created by the requestor):
121
121
 
122
122
  ```bash
123
123
  poetry run golem vm create my-vm \
@@ -128,15 +128,30 @@ poetry run golem vm create my-vm \
128
128
 
129
129
  Environment (env prefix `GOLEM_REQUESTOR_`):
130
130
 
131
- - `polygon_rpc_url` — Polygon PoS RPC URL
132
- - `stream_payment_address` — StreamPayment address
133
- - `glm_token_address` — GLM ERC20 address
134
- - `provider_eth_address` — optional helper for development; in production always use `/provider/info`
131
+ - `polygon_rpc_url` — EVM RPC URL (default L2 RPC)
132
+ - `stream_payment_address` — StreamPayment address (fallback if provider doesn’t advertise)
133
+ - `glm_token_address` — Token address; set to zero address to use native ETH
134
+ - `provider_eth_address` — optional dev helper; in production always use `/provider/info`
135
+ - `network` — Target network for discovery filtering: `testnet` (default) or `mainnet`
135
136
 
136
137
  Efficiency tips:
137
138
 
138
139
  - Batch top‑ups (e.g., add several hours at once) to reduce on‑chain calls.
139
140
  - Withdrawals are typically executed by providers; requestors don’t need to withdraw.
141
+ - The CLI `vm stream open` will prefer the provider’s advertised contract/token addresses to prevent mismatches.
142
+
143
+ ## Faucet (L2 ETH)
144
+
145
+ - Request L2 test ETH to cover stream transactions:
146
+
147
+ ```bash
148
+ poetry run golem wallet faucet
149
+ ```
150
+
151
+ - Defaults:
152
+ - Faucet: `https://l2.holesky.golemdb.io/faucet`
153
+ - CAPTCHA: `https://cap.gobas.me/05381a2cef5e`
154
+ - Override with env: `GOLEM_REQUESTOR_l2_faucet_url`, `GOLEM_REQUESTOR_captcha_url`, `GOLEM_REQUESTOR_captcha_api_key`.
140
155
 
141
156
  ## Installation
142
157
 
@@ -178,6 +193,27 @@ Alternatively, you can prepend the environment variables directly to the command
178
193
  GOLEM_REQUESTOR_ENVIRONMENT="development" GOLEM_REQUESTOR_FORCE_LOCALHOST="true" poetry run golem vm providers
179
194
  ```
180
195
 
196
+ ### Mode vs. Network
197
+
198
+ - Development Mode (`GOLEM_REQUESTOR_ENVIRONMENT=development`)
199
+ - Improves local workflows: prefixes central discovery URL with `DEVMODE-` and, when using the central driver, maps provider IPs to `localhost` for easier testing.
200
+ - Does not determine chain selection.
201
+
202
+ - Network Selection (`--network` or `GOLEM_REQUESTOR_NETWORK`)
203
+ - Filters Golem Base discovery results by `golem_network=testnet|mainnet`.
204
+ - Combine with the appropriate RPC envs (`GOLEM_REQUESTOR_GOLEM_BASE_RPC_URL`, `GOLEM_REQUESTOR_GOLEM_BASE_WS_URL`) and any contract addresses.
205
+ - Independent from dev ergonomics.
206
+
207
+ Examples:
208
+ - List providers on mainnet without changing env:
209
+ ```bash
210
+ poetry run golem vm providers --network mainnet
211
+ ```
212
+ - Create a VM while targeting testnet:
213
+ ```bash
214
+ poetry run golem vm create my-vm --provider-id 0xProvider --cpu 2 --memory 4 --storage 20 --network testnet
215
+ ```
216
+
181
217
  ## Usage
182
218
 
183
219
  ### Provider Discovery
@@ -295,6 +331,7 @@ export GOLEM_REQUESTOR_DB_PATH="/path/to/database.db"
295
331
  # Environment Mode (defaults to "production")
296
332
  export GOLEM_REQUESTOR_ENVIRONMENT="development" # Optional: Switch to development mode
297
333
  export GOLEM_REQUESTOR_FORCE_LOCALHOST="true" # Optional: Force localhost in development mode
334
+ export GOLEM_REQUESTOR_NETWORK="testnet" # Or "mainnet"; filters Golem Base results by annotation
298
335
  ```
299
336
 
300
337
  2. Directory Structure:
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "request-vm-on-golem"
3
- version = "0.1.41"
3
+ version = "0.1.44"
4
4
  description = "VM on Golem Requestor CLI - Create and manage virtual machines on the Golem Network"
5
5
  authors = ["Phillip Jensen <phillip+vm-on-golem@golemgrid.com>"]
6
6
  readme = "README.md"
@@ -39,10 +39,12 @@ golem-base-sdk = "==0.1.0"
39
39
  httptools = "^0.6.0"
40
40
  pydantic-settings = "^2.1.0"
41
41
  web3 = "==7.13.0"
42
+ golem-streaming-abi = ">=0.1.0,<0.2.0"
43
+ golem-faucet = ">=0.1.0,<0.2.0"
42
44
 
43
45
  [tool.poetry.group.dev.dependencies]
44
- pytest = "^7.0.0"
45
- pytest-asyncio = "^0.18.0"
46
+ pytest = "^8.2.0"
47
+ pytest-asyncio = "^1.1.0"
46
48
  pytest-cov = "^3.0.0"
47
49
  httpx = "^0.23.0"
48
50
  black = "^22.3.0"
@@ -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
@@ -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",
@@ -0,0 +1,161 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Optional, Any, Dict
5
+
6
+ from web3 import Web3
7
+ from eth_account import Account
8
+ from golem_streaming_abi import STREAM_PAYMENT_ABI, ERC20_ABI
9
+
10
+
11
+ @dataclass
12
+ class StreamPaymentConfig:
13
+ rpc_url: str
14
+ contract_address: str
15
+ glm_token_address: str
16
+ private_key: str
17
+
18
+
19
+ class StreamPaymentClient:
20
+ def __init__(self, cfg: StreamPaymentConfig):
21
+ self.web3 = Web3(Web3.HTTPProvider(cfg.rpc_url))
22
+ self.account = Account.from_key(cfg.private_key)
23
+ self.web3.eth.default_account = self.account.address
24
+
25
+ self.contract = self.web3.eth.contract(
26
+ address=Web3.to_checksum_address(cfg.contract_address), abi=STREAM_PAYMENT_ABI
27
+ )
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
+ )
35
+
36
+ def _send(self, fn) -> Dict[str, Any]:
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)
66
+ # In production, sign and send raw; in tests, Account may be a dummy without signer
67
+ if hasattr(self.account, "sign_transaction"):
68
+ signed = self.account.sign_transaction(tx)
69
+ tx_hash = self.web3.eth.send_raw_transaction(signed.rawTransaction)
70
+ else:
71
+ tx_hash = self.web3.eth.send_transaction(tx)
72
+ receipt = self.web3.eth.wait_for_transaction_receipt(tx_hash)
73
+ return {"transactionHash": tx_hash.hex(), "status": receipt.status, "logs": receipt.logs}
74
+
75
+ def create_stream(self, provider_address: str, deposit_wei: int, rate_per_second_wei: int) -> int:
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)
90
+
91
+ # Create stream
92
+ fn = self.contract.functions.createStream(
93
+ token_param,
94
+ Web3.to_checksum_address(provider_address),
95
+ int(deposit_wei),
96
+ int(rate_per_second_wei),
97
+ )
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)
113
+
114
+ # Try to parse StreamCreated event for streamId
115
+ try:
116
+ for log in tx_receipt["logs"]:
117
+ # very naive filter: topic0 = keccak256(StreamCreated(...))
118
+ # When ABI is attached to contract, use contract.events
119
+ ev = self.contract.events.StreamCreated().process_log(log)
120
+ return int(ev["args"]["streamId"])
121
+ except Exception:
122
+ pass
123
+ # As a fallback, cannot easily fetch return value from a tx; caller should query later
124
+ raise RuntimeError("create_stream: could not parse streamId from receipt")
125
+
126
+ def withdraw(self, stream_id: int) -> str:
127
+ fn = self.contract.functions.withdraw(int(stream_id))
128
+ receipt = self._send(fn)
129
+ return receipt["transactionHash"]
130
+
131
+ def terminate(self, stream_id: int) -> str:
132
+ fn = self.contract.functions.terminate(int(stream_id))
133
+ receipt = self._send(fn)
134
+ return receipt["transactionHash"]
135
+
136
+ def top_up(self, stream_id: int, amount_wei: int) -> str:
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:
@@ -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 = {
@@ -1,148 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from dataclasses import dataclass
4
- from typing import Optional, Any, Dict
5
-
6
- from web3 import Web3
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
- ]
66
-
67
-
68
- @dataclass
69
- class StreamPaymentConfig:
70
- rpc_url: str
71
- contract_address: str
72
- glm_token_address: str
73
- private_key: str
74
-
75
-
76
- class StreamPaymentClient:
77
- def __init__(self, cfg: StreamPaymentConfig):
78
- self.web3 = Web3(Web3.HTTPProvider(cfg.rpc_url))
79
- self.account = Account.from_key(cfg.private_key)
80
- self.web3.eth.default_account = self.account.address
81
-
82
- self.contract = self.web3.eth.contract(
83
- address=Web3.to_checksum_address(cfg.contract_address), abi=STREAM_PAYMENT_ABI
84
- )
85
- self.erc20 = self.web3.eth.contract(
86
- address=Web3.to_checksum_address(cfg.glm_token_address), abi=ERC20_ABI
87
- )
88
-
89
- 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
- )
96
- # In production, sign and send raw; in tests, Account may be a dummy without signer
97
- if hasattr(self.account, "sign_transaction"):
98
- signed = self.account.sign_transaction(tx)
99
- tx_hash = self.web3.eth.send_raw_transaction(signed.rawTransaction)
100
- else:
101
- tx_hash = self.web3.eth.send_transaction(tx)
102
- receipt = self.web3.eth.wait_for_transaction_receipt(tx_hash)
103
- return {"transactionHash": tx_hash.hex(), "status": receipt.status, "logs": receipt.logs}
104
-
105
- 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)
109
-
110
- # 2) Create stream
111
- fn = self.contract.functions.createStream(
112
- self.erc20.address,
113
- Web3.to_checksum_address(provider_address),
114
- int(deposit_wei),
115
- int(rate_per_second_wei),
116
- )
117
- receipt = self._send(fn)
118
-
119
- # Try to parse StreamCreated event for streamId
120
- try:
121
- for log in receipt["logs"]:
122
- # very naive filter: topic0 = keccak256(StreamCreated(...))
123
- # When ABI is attached to contract, use contract.events
124
- ev = self.contract.events.StreamCreated().process_log(log)
125
- return int(ev["args"]["streamId"])
126
- except Exception:
127
- pass
128
- # As a fallback, cannot easily fetch return value from a tx; caller should query later
129
- raise RuntimeError("create_stream: could not parse streamId from receipt")
130
-
131
- def withdraw(self, stream_id: int) -> str:
132
- fn = self.contract.functions.withdraw(int(stream_id))
133
- receipt = self._send(fn)
134
- return receipt["transactionHash"]
135
-
136
- def terminate(self, stream_id: int) -> str:
137
- fn = self.contract.functions.terminate(int(stream_id))
138
- receipt = self._send(fn)
139
- return receipt["transactionHash"]
140
-
141
- 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"]