4
4
import json
5
5
import logging
6
6
import shutil
7
+ from decimal import Decimal
7
8
from ipaddress import IPv6Interface
9
+ from math import ceil
8
10
from pathlib import Path
9
11
from typing import Dict , List , Optional , Tuple , Union , cast
10
12
13
+ import aiohttp
11
14
import typer
12
15
from aiohttp import ClientConnectorError , ClientResponseError , ClientSession
13
16
from aleph .sdk import AlephHttpClient , AuthenticatedAlephHttpClient
14
17
from aleph .sdk .account import _load_account
18
+ from aleph .sdk .chains .ethereum import ETHAccount
15
19
from aleph .sdk .client .vm_client import VmClient
16
20
from aleph .sdk .client .vm_confidential_client import VmConfidentialClient
17
21
from aleph .sdk .conf import settings as sdk_settings
21
25
MessageNotFoundError ,
22
26
)
23
27
from aleph .sdk .query .filters import MessageFilter
28
+ from aleph .sdk .query .responses import PriceResponse
24
29
from aleph .sdk .types import AccountFromPrivateKey , StorageEnum
25
30
from aleph .sdk .utils import calculate_firmware_hash
26
31
from aleph_message .models import InstanceMessage , StoreMessage
49
54
setup_logging ,
50
55
validated_int_prompt ,
51
56
validated_prompt ,
57
+ wait_for_confirmed_flow ,
58
+ wait_for_processed_instance ,
52
59
)
53
60
from aleph_client .conf import settings
54
61
from aleph_client .models import CRNInfo
55
62
from aleph_client .utils import AsyncTyper , fetch_json
56
63
57
64
from ..utils import has_nested_attr
65
+ from .superfluid import FlowUpdate , update_flow
58
66
59
67
logger = logging .getLogger (__name__ )
60
68
app = AsyncTyper (no_args_is_help = True )
63
71
@app .command ()
64
72
async def create (
65
73
payment_type : PaymentType = typer .Option (None , help = help_strings .PAYMENT_TYPE ),
74
+ payment_chain : Chain = typer .Option (None , help = help_strings .PAYMENT_CHAIN ),
66
75
hypervisor : HypervisorType = typer .Option (None , help = help_strings .HYPERVISOR ),
67
76
name : Optional [str ] = typer .Option (None , help = help_strings .INSTANCE_NAME ),
68
- rootfs : str = typer .Option ("ubuntu22" , help = help_strings .ROOTFS ),
77
+ rootfs : str = typer .Option (None , help = help_strings .ROOTFS ),
69
78
rootfs_size : int = typer .Option (None , help = help_strings .ROOTFS_SIZE ),
70
79
vcpus : int = typer .Option (None , help = help_strings .VCPUS ),
71
80
memory : int = typer .Option (None , help = help_strings .MEMORY ),
@@ -133,6 +142,28 @@ def validate_ssh_pubkey_file(file: Union[str, Path]) -> Path:
133
142
)
134
143
is_stream = payment_type != PaymentType .hold
135
144
145
+ # super_token_chains = get_chains_with_super_token()
146
+ super_token_chains = [Chain .AVAX .value ]
147
+ if is_stream :
148
+ if payment_chain is None or payment_chain not in super_token_chains :
149
+ payment_chain = Chain (
150
+ Prompt .ask (
151
+ "Which chain do you want to use for Pay-As-You-Go?" ,
152
+ choices = super_token_chains ,
153
+ default = Chain .AVAX .value ,
154
+ )
155
+ )
156
+ if isinstance (account , ETHAccount ):
157
+ account .switch_chain (payment_chain )
158
+ if account .superfluid_connector : # Quick check with theoretical min price
159
+ try :
160
+ account .superfluid_connector .can_start_flow (Decimal (0.000031 )) # 0.11/h
161
+ except Exception as e :
162
+ echo (e )
163
+ raise typer .Exit (code = 1 )
164
+ else :
165
+ payment_chain = Chain .ETH # Hold chain for all balances
166
+
136
167
if confidential :
137
168
if hypervisor and hypervisor != HypervisorType .qemu :
138
169
echo ("Only QEMU is supported as an hypervisor for confidential" )
@@ -171,7 +202,7 @@ def validate_ssh_pubkey_file(file: Union[str, Path]) -> Path:
171
202
if confidential :
172
203
# Confidential only support custom rootfs
173
204
rootfs = "custom"
174
- elif rootfs not in os_choices :
205
+ elif not rootfs or rootfs not in os_choices :
175
206
rootfs = Prompt .ask (
176
207
"Use a custom rootfs or one of the following prebuilt ones:" ,
177
208
default = rootfs ,
@@ -206,6 +237,7 @@ def validate_ssh_pubkey_file(file: Union[str, Path]) -> Path:
206
237
if not firmware_message :
207
238
echo ("Confidential Firmware hash does not exist on aleph.im" )
208
239
raise typer .Exit (code = 1 )
240
+
209
241
name = name or validated_prompt ("Instance name" , lambda x : len (x ) < 65 )
210
242
rootfs_size = rootfs_size or validated_int_prompt (
211
243
"Disk size in MiB" , default = settings .DEFAULT_ROOTFS_SIZE , min_value = 10_240 , max_value = 102_400
@@ -228,27 +260,24 @@ def validate_ssh_pubkey_file(file: Union[str, Path]) -> Path:
228
260
immutable_volume = immutable_volume ,
229
261
)
230
262
231
- # For PAYG or confidential, the user select directly the node on which to run on
232
- # For PAYG User have to make the payment stream separately
233
- # For now, we allow hold for confidential, but the user still has to choose on which CRN to run.
234
263
stream_reward_address = None
235
264
crn = None
236
265
if crn_url and crn_hash :
237
266
crn_url = sanitize_url (crn_url )
238
267
try :
239
- name , score , reward_addr = "?" , 0 , ""
268
+ crn_name , score , reward_addr = "?" , 0 , ""
240
269
nodes : NodeInfo = await _fetch_nodes ()
241
270
for node in nodes .nodes :
242
271
if node ["address" ].rstrip ("/" ) == crn_url :
243
- name = node ["name" ]
272
+ crn_name = node ["name" ]
244
273
score = node ["score" ]
245
274
reward_addr = node ["stream_reward" ]
246
275
break
247
276
crn_info = await fetch_crn_info (crn_url )
248
277
if crn_info :
249
278
crn = CRNInfo (
250
279
hash = ItemHash (crn_hash ),
251
- name = name or "?" ,
280
+ name = crn_name or "?" ,
252
281
url = crn_url ,
253
282
version = crn_info .get ("version" , "" ),
254
283
score = score ,
@@ -293,13 +322,11 @@ def validate_ssh_pubkey_file(file: Union[str, Path]) -> Path:
293
322
raise typer .Exit (1 )
294
323
295
324
async with AuthenticatedAlephHttpClient (account = account , api_server = sdk_settings .API_HOST ) as client :
296
- payment : Optional [Payment ] = None
297
- if stream_reward_address :
298
- payment = Payment (
299
- chain = Chain .AVAX ,
300
- receiver = stream_reward_address ,
301
- type = payment_type ,
302
- )
325
+ payment = Payment (
326
+ chain = payment_chain ,
327
+ receiver = stream_reward_address if stream_reward_address else None ,
328
+ type = payment_type ,
329
+ )
303
330
try :
304
331
message , status = await client .create_instance (
305
332
sync = True ,
@@ -341,7 +368,36 @@ def validate_ssh_pubkey_file(file: Union[str, Path]) -> Path:
341
368
# Not the ideal solution
342
369
logger .debug (f"Cannot allocate { item_hash } : no CRN url" )
343
370
return item_hash , crn_url
344
- account = _load_account (private_key , private_key_file )
371
+
372
+ # Wait for the instance message to be processed
373
+ async with aiohttp .ClientSession () as session :
374
+ await wait_for_processed_instance (session , item_hash )
375
+
376
+ # Pay-As-You-Go
377
+ if payment_type == PaymentType .superfluid :
378
+ price : PriceResponse = await client .get_program_price (item_hash )
379
+ ceil_factor = 10 ** 18
380
+ required_tokens = ceil (Decimal (price .required_tokens ) * ceil_factor ) / ceil_factor
381
+ if isinstance (account , ETHAccount ) and account .superfluid_connector :
382
+ try : # Double check with effective price
383
+ account .superfluid_connector .can_start_flow (Decimal (0.000031 )) # Min for 0.11/h
384
+ except Exception as e :
385
+ echo (e )
386
+ raise typer .Exit (code = 1 )
387
+ flow_hash = await update_flow (
388
+ account = account ,
389
+ receiver = crn .stream_reward_address ,
390
+ flow = Decimal (required_tokens ),
391
+ update_type = FlowUpdate .INCREASE ,
392
+ )
393
+ # Wait for the flow transaction to be confirmed
394
+ await wait_for_confirmed_flow (account , message .content .payment .receiver )
395
+ if flow_hash :
396
+ echo (
397
+ f"Flow { flow_hash } has been created:\n \t - price/sec: { price .required_tokens :.7f} ALEPH\n \t - receiver: { crn .stream_reward_address } "
398
+ )
399
+
400
+ # Notify CRN
345
401
async with VmClient (account , crn .url ) as crn_client :
346
402
status , result = await crn_client .start_instance (vm_id = item_hash )
347
403
logger .debug (status , result )
@@ -437,6 +493,20 @@ async def delete(
437
493
echo ("You are not the owner of this instance" )
438
494
raise typer .Exit (code = 1 )
439
495
496
+ # Check for streaming payment and eventually stop it
497
+ payment : Optional [Payment ] = existing_message .content .payment
498
+ if payment is not None and payment .type == PaymentType .superfluid :
499
+ price : PriceResponse = await client .get_program_price (item_hash )
500
+ if payment .receiver is not None :
501
+ if isinstance (account , ETHAccount ):
502
+ account .switch_chain (payment .chain )
503
+ if account .superfluid_connector :
504
+ flow_hash = await update_flow (
505
+ account , payment .receiver , Decimal (price .required_tokens ), FlowUpdate .REDUCE
506
+ )
507
+ if flow_hash :
508
+ echo (f"Flow { flow_hash } has been deleted." )
509
+
440
510
# Check status of the instance and eventually erase associated VM
441
511
node_list : NodeInfo = await _fetch_nodes ()
442
512
_ , details = await _get_instance_details (existing_message , node_list )
@@ -962,6 +1032,7 @@ async def confidential(
962
1032
keep_session : bool = typer .Option (None , help = help_strings .KEEP_SESSION ),
963
1033
vm_secret : str = typer .Option (None , help = help_strings .VM_SECRET ),
964
1034
payment_type : PaymentType = typer .Option (None , help = help_strings .PAYMENT_TYPE ),
1035
+ payment_chain : Optional [Chain ] = typer .Option (None , help = help_strings .PAYMENT_CHAIN ),
965
1036
name : Optional [str ] = typer .Option (None , help = help_strings .INSTANCE_NAME ),
966
1037
rootfs : str = typer .Option ("ubuntu22" , help = help_strings .ROOTFS ),
967
1038
rootfs_size : int = typer .Option (None , help = help_strings .ROOTFS_SIZE ),
@@ -1002,6 +1073,7 @@ async def confidential(
1002
1073
if not vm_id or len (vm_id ) != 64 :
1003
1074
vm_id , crn_url = await create (
1004
1075
payment_type ,
1076
+ payment_chain ,
1005
1077
None ,
1006
1078
name ,
1007
1079
rootfs ,
0 commit comments