7
7
from collections .abc import Mapping
8
8
from decimal import Decimal
9
9
from pathlib import Path
10
- from typing import Annotated , Any , Optional , cast
10
+ from typing import Annotated , Any , Optional , Union , cast
11
11
from zipfile import BadZipFile
12
12
13
13
import aiohttp
14
14
import typer
15
15
from aleph .sdk import AlephHttpClient , AuthenticatedAlephHttpClient
16
16
from aleph .sdk .account import _load_account
17
17
from aleph .sdk .client .vm_client import VmClient
18
- from aleph .sdk .conf import settings
18
+ from aleph .sdk .conf import load_main_configuration , settings
19
+ from aleph .sdk .evm_utils import get_chains_with_holding
19
20
from aleph .sdk .exceptions import (
20
21
ForgottenMessageError ,
21
22
InsufficientFundsError ,
24
25
from aleph .sdk .query .filters import MessageFilter
25
26
from aleph .sdk .query .responses import PriceResponse
26
27
from aleph .sdk .types import AccountFromPrivateKey , StorageEnum , TokenType
27
- from aleph .sdk .utils import make_program_content , safe_getattr
28
- from aleph_message .models import Chain , MessageType , ProgramMessage , StoreMessage
28
+ from aleph .sdk .utils import displayable_amount , make_program_content , safe_getattr
29
+ from aleph_message .models import (
30
+ Chain ,
31
+ MessageType ,
32
+ Payment ,
33
+ PaymentType ,
34
+ ProgramMessage ,
35
+ StoreMessage ,
36
+ )
29
37
from aleph_message .models .execution .program import ProgramContent
30
38
from aleph_message .models .item_hash import ItemHash
31
39
from aleph_message .status import MessageStatus
55
63
logger = logging .getLogger (__name__ )
56
64
app = AsyncTyper (no_args_is_help = True )
57
65
66
+ hold_chains = [* get_chains_with_holding (), Chain .SOL ]
67
+ metavar_valid_chains = f"[{ '|' .join (hold_chains )} ]"
68
+
58
69
59
70
@app .command (name = "upload" )
60
71
@app .command (name = "create" )
@@ -80,6 +91,14 @@ async def upload(
80
91
skip_env_var : Annotated [bool , typer .Option (help = help_strings .SKIP_ENV_VAR )] = False ,
81
92
env_vars : Annotated [Optional [str ], typer .Option (help = help_strings .ENVIRONMENT_VARIABLES )] = None ,
82
93
address : Annotated [Optional [str ], typer .Option (help = help_strings .ADDRESS_PAYER )] = None ,
94
+ payment_chain : Annotated [
95
+ Optional [Chain ],
96
+ typer .Option (
97
+ help = help_strings .PAYMENT_CHAIN_PROGRAM ,
98
+ metavar = metavar_valid_chains ,
99
+ case_sensitive = False ,
100
+ ),
101
+ ] = None ,
83
102
channel : Annotated [Optional [str ], typer .Option (help = help_strings .CHANNEL )] = settings .DEFAULT_CHANNEL ,
84
103
private_key : Annotated [Optional [str ], typer .Option (help = help_strings .PRIVATE_KEY )] = settings .PRIVATE_KEY_STRING ,
85
104
private_key_file : Annotated [
@@ -96,7 +115,7 @@ async def upload(
96
115
For more information, see https://docs.aleph.im/computing"""
97
116
98
117
setup_logging (debug )
99
-
118
+ console = Console ()
100
119
path = path .absolute ()
101
120
102
121
try :
@@ -108,9 +127,25 @@ async def upload(
108
127
typer .echo ("No such file or directory" )
109
128
raise typer .Exit (code = 4 ) from error
110
129
111
- account : AccountFromPrivateKey = _load_account (private_key , private_key_file )
130
+ account : AccountFromPrivateKey = _load_account (private_key , private_key_file , chain = payment_chain )
112
131
address = address or settings .ADDRESS_TO_USE or account .get_address ()
113
132
133
+ # Loads default configuration if no chain is set
134
+ if payment_chain is None :
135
+ config = load_main_configuration (settings .CONFIG_FILE )
136
+ if config is not None :
137
+ payment_chain = config .chain
138
+ console .print (f"Preset to default chain: [green]{ payment_chain } [/green]" )
139
+ else :
140
+ payment_chain = Chain .ETH
141
+ console .print ("No active chain selected in configuration. Fallback to ETH" )
142
+
143
+ payment = Payment (
144
+ chain = payment_chain ,
145
+ receiver = None ,
146
+ type = PaymentType .hold ,
147
+ )
148
+
114
149
async with AuthenticatedAlephHttpClient (account = account , api_server = settings .API_HOST ) as client :
115
150
# Upload the source code
116
151
with open (path_object , "rb" ) as fd :
@@ -181,6 +216,7 @@ async def upload(
181
216
"runtime" : runtime ,
182
217
"metadata" : {"name" : name },
183
218
"address" : address ,
219
+ "payment" : payment ,
184
220
"vcpus" : vcpus ,
185
221
"memory" : memory ,
186
222
"timeout_seconds" : timeout_seconds ,
@@ -235,8 +271,6 @@ async def upload(
235
271
hash_base32 = b32encode (b16decode (item_hash .upper ())).strip (b"=" ).lower ().decode ()
236
272
func_url_1 = f"{ settings .VM_URL_PATH .format (hash = item_hash )} "
237
273
func_url_2 = f"{ settings .VM_URL_HOST .format (hash_base32 = hash_base32 )} "
238
-
239
- console = Console ()
240
274
infos = [
241
275
Text .from_markup (f"Your program [bright_cyan]{ item_hash } [/bright_cyan] has been uploaded on aleph.im." ),
242
276
Text .assemble (
@@ -271,6 +305,9 @@ async def upload(
271
305
async def update (
272
306
item_hash : Annotated [str , typer .Argument (help = "Item hash to update" )],
273
307
path : Annotated [Path , typer .Argument (help = help_strings .PROGRAM_PATH )],
308
+ chain : Annotated [
309
+ Optional [Chain ], typer .Option (help = help_strings .PAYMENT_CHAIN_PROGRAM_USED , metavar = metavar_valid_chains )
310
+ ] = None ,
274
311
private_key : Annotated [Optional [str ], typer .Option (help = help_strings .PRIVATE_KEY )] = settings .PRIVATE_KEY_STRING ,
275
312
private_key_file : Annotated [
276
313
Optional [Path ], typer .Option (help = help_strings .PRIVATE_KEY_FILE )
@@ -294,7 +331,7 @@ async def update(
294
331
typer .echo ("No such file or directory" )
295
332
raise typer .Exit (code = 4 ) from error
296
333
297
- account : AccountFromPrivateKey = _load_account (private_key , private_key_file )
334
+ account : AccountFromPrivateKey = _load_account (private_key , private_key_file , chain = chain )
298
335
299
336
async with AuthenticatedAlephHttpClient (account = account , api_server = settings .API_HOST ) as client :
300
337
try :
@@ -381,6 +418,9 @@ async def delete(
381
418
item_hash : Annotated [str , typer .Argument (help = "Item hash to unpersist" )],
382
419
reason : Annotated [str , typer .Option (help = "Reason for deleting the program" )] = "User deletion" ,
383
420
keep_code : Annotated [bool , typer .Option (help = help_strings .PROGRAM_KEEP_CODE )] = False ,
421
+ chain : Annotated [
422
+ Optional [Chain ], typer .Option (help = help_strings .PAYMENT_CHAIN_PROGRAM_USED , metavar = metavar_valid_chains )
423
+ ] = None ,
384
424
private_key : Annotated [Optional [str ], typer .Option (help = help_strings .PRIVATE_KEY )] = settings .PRIVATE_KEY_STRING ,
385
425
private_key_file : Annotated [
386
426
Optional [Path ], typer .Option (help = help_strings .PRIVATE_KEY_FILE )
@@ -393,7 +433,7 @@ async def delete(
393
433
394
434
setup_logging (debug )
395
435
396
- account = _load_account (private_key , private_key_file )
436
+ account = _load_account (private_key , private_key_file , chain = chain )
397
437
398
438
async with AuthenticatedAlephHttpClient (account = account , api_server = settings .API_HOST ) as client :
399
439
try :
@@ -440,6 +480,9 @@ async def delete(
440
480
@app .command (name = "list" )
441
481
async def list_programs (
442
482
address : Annotated [Optional [str ], typer .Option (help = "Owner address of the programs" )] = None ,
483
+ chain : Annotated [
484
+ Optional [Chain ], typer .Option (help = help_strings .ADDRESS_CHAIN , metavar = metavar_valid_chains )
485
+ ] = None ,
443
486
private_key : Annotated [Optional [str ], typer .Option (help = help_strings .PRIVATE_KEY )] = settings .PRIVATE_KEY_STRING ,
444
487
private_key_file : Annotated [
445
488
Optional [Path ], typer .Option (help = help_strings .PRIVATE_KEY_FILE )
@@ -451,7 +494,7 @@ async def list_programs(
451
494
452
495
setup_logging (debug )
453
496
454
- account = _load_account (private_key , private_key_file )
497
+ account = _load_account (private_key , private_key_file , chain = chain )
455
498
address = address or settings .ADDRESS_TO_USE or account .get_address ()
456
499
457
500
async with AlephHttpClient (api_server = settings .API_HOST ) as client :
@@ -493,29 +536,74 @@ async def list_programs(
493
536
)
494
537
msg_link = f"https://explorer.aleph.im/address/ETH/{ message .sender } /message/PROGRAM/{ message .item_hash } "
495
538
item_hash_link = Text .from_markup (f"[link={ msg_link } ]{ message .item_hash } [/link]" , style = "bright_cyan" )
539
+ payment_type = safe_getattr (message .content , "payment.type" , PaymentType .hold )
540
+ payment = Text .assemble (
541
+ "Payment: " ,
542
+ Text (
543
+ payment_type .capitalize ().ljust (12 ),
544
+ style = "red" if payment_type == PaymentType .hold else "orange3" ,
545
+ ),
546
+ )
547
+ persistent = Text .assemble (
548
+ "Type: " ,
549
+ (
550
+ Text ("Persistent" , style = "green" )
551
+ if message .content .on .persistent
552
+ else Text ("Ephemeral" , style = "grey50" )
553
+ ),
554
+ )
555
+ payment_chain = str (safe_getattr (message .content , "payment.chain.value" ) or Chain .ETH .value )
556
+ if payment_chain != Chain .SOL .value :
557
+ payment_chain = "EVM"
558
+ pay_chain = Text .assemble ("Chain: " , Text (payment_chain .ljust (14 ), style = "white" ))
496
559
created_at = Text .assemble (
497
- "URLs ↓ \t Created at: " ,
560
+ "Created at: " ,
498
561
Text (
499
562
str (str_to_datetime (str (safe_getattr (message , "content.time" )))).split ("." , maxsplit = 1 )[0 ],
500
563
style = "orchid" ,
501
564
),
502
565
)
566
+ payer : Union [str , Text ] = ""
567
+ if message .sender != message .content .address :
568
+ payer = Text .assemble ("\n Payer: " , Text (str (message .content .address ), style = "orange1" ))
569
+ price : PriceResponse = await client .get_program_price (message .item_hash )
570
+ required_tokens = Decimal (price .required_tokens )
571
+ if price .payment_type == PaymentType .hold .value :
572
+ aleph_price = Text (
573
+ f"{ displayable_amount (required_tokens , decimals = 3 )} (fixed)" .ljust (13 ), style = "violet"
574
+ )
575
+ else :
576
+ # PAYG not implemented yet for programs
577
+ aleph_price = Text ("" )
578
+ cost = Text .assemble ("\n $ALEPH: " , aleph_price )
503
579
hash_base32 = b32encode (b16decode (message .item_hash .upper ())).strip (b"=" ).lower ().decode ()
504
580
func_url_1 = settings .VM_URL_PATH .format (hash = message .item_hash )
505
581
func_url_2 = settings .VM_URL_HOST .format (hash_base32 = hash_base32 )
506
582
urls = Text .from_markup (
507
- f"[bright_yellow][link={ func_url_1 } ]{ func_url_1 } [/link][/bright_yellow]\n [dark_olive_green2][link={ func_url_2 } ]{ func_url_2 } [/link][/dark_olive_green2]"
583
+ f"URLs ↓\n [bright_yellow][link={ func_url_1 } ]{ func_url_1 } [/link][/bright_yellow]"
584
+ f"\n [dark_olive_green2][link={ func_url_2 } ]{ func_url_2 } [/link][/dark_olive_green2]"
508
585
)
509
586
program = Text .assemble (
510
- "Item Hash ↓\t Name: " , name , "\n " , item_hash_link , "\n " , created_at , "\n " , urls
587
+ "Item Hash ↓\t Name: " ,
588
+ name ,
589
+ "\n " ,
590
+ item_hash_link ,
591
+ "\n " ,
592
+ payment ,
593
+ persistent ,
594
+ "\n " ,
595
+ pay_chain ,
596
+ created_at ,
597
+ payer ,
598
+ cost ,
599
+ urls ,
511
600
)
512
601
specs = [
513
602
f"vCPU: [magenta3]{ message .content .resources .vcpus } [/magenta3]\n " ,
514
603
f"RAM: [magenta3]{ message .content .resources .memory / 1_024 :.2f} GiB[/magenta3]\n " ,
515
604
"HyperV: [magenta3]Firecracker[/magenta3]\n " ,
516
605
f"Timeout: [orange3]{ message .content .resources .seconds } s[/orange3]\n " ,
517
606
f"Internet: { '[green]Yes[/green]' if message .content .environment .internet else '[red]No[/red]' } \n " ,
518
- f"Persistent: { '[green]Yes[/green]' if message .content .on .persistent else '[red]No[/red]' } \n " ,
519
607
f"Updatable: { '[green]Yes[/green]' if message .content .allow_amend else '[orange3]Code only[/orange3]' } " ,
520
608
]
521
609
specifications = Text .from_markup ("" .join (specs ))
@@ -568,6 +656,9 @@ async def list_programs(
568
656
async def persist (
569
657
item_hash : Annotated [str , typer .Argument (help = "Item hash to persist" )],
570
658
keep_prev : Annotated [bool , typer .Option (help = help_strings .PROGRAM_KEEP_PREV )] = False ,
659
+ chain : Annotated [
660
+ Optional [Chain ], typer .Option (help = help_strings .PAYMENT_CHAIN_PROGRAM_USED , metavar = metavar_valid_chains )
661
+ ] = None ,
571
662
private_key : Annotated [Optional [str ], typer .Option (help = help_strings .PRIVATE_KEY )] = settings .PRIVATE_KEY_STRING ,
572
663
private_key_file : Annotated [
573
664
Optional [Path ], typer .Option (help = help_strings .PRIVATE_KEY_FILE )
@@ -582,7 +673,7 @@ async def persist(
582
673
583
674
setup_logging (debug )
584
675
585
- account = _load_account (private_key , private_key_file )
676
+ account = _load_account (private_key , private_key_file , chain = chain )
586
677
587
678
async with AuthenticatedAlephHttpClient (account = account , api_server = settings .API_HOST ) as client :
588
679
try :
@@ -662,6 +753,9 @@ async def persist(
662
753
async def unpersist (
663
754
item_hash : Annotated [str , typer .Argument (help = "Item hash to unpersist" )],
664
755
keep_prev : Annotated [bool , typer .Option (help = help_strings .PROGRAM_KEEP_PREV )] = False ,
756
+ chain : Annotated [
757
+ Optional [Chain ], typer .Option (help = help_strings .PAYMENT_CHAIN_PROGRAM_USED , metavar = metavar_valid_chains )
758
+ ] = None ,
665
759
private_key : Annotated [Optional [str ], typer .Option (help = help_strings .PRIVATE_KEY )] = settings .PRIVATE_KEY_STRING ,
666
760
private_key_file : Annotated [
667
761
Optional [Path ], typer .Option (help = help_strings .PRIVATE_KEY_FILE )
@@ -676,7 +770,7 @@ async def unpersist(
676
770
677
771
setup_logging (debug )
678
772
679
- account = _load_account (private_key , private_key_file )
773
+ account = _load_account (private_key , private_key_file , chain = chain )
680
774
681
775
async with AuthenticatedAlephHttpClient (account = account , api_server = settings .API_HOST ) as client :
682
776
try :
@@ -755,12 +849,14 @@ async def unpersist(
755
849
@app .command ()
756
850
async def logs (
757
851
item_hash : Annotated [str , typer .Argument (help = "Item hash of program" )],
852
+ chain : Annotated [
853
+ Optional [Chain ], typer .Option (help = help_strings .PAYMENT_CHAIN_PROGRAM_USED , metavar = metavar_valid_chains )
854
+ ] = None ,
758
855
private_key : Annotated [Optional [str ], typer .Option (help = help_strings .PRIVATE_KEY )] = settings .PRIVATE_KEY_STRING ,
759
856
private_key_file : Annotated [
760
857
Optional [Path ], typer .Option (help = help_strings .PRIVATE_KEY_FILE )
761
858
] = settings .PRIVATE_KEY_FILE ,
762
859
domain : Annotated [Optional [str ], typer .Option (help = help_strings .PROMPT_PROGRAM_CRN_URL )] = None ,
763
- chain : Annotated [Optional [Chain ], typer .Option (help = help_strings .ADDRESS_CHAIN )] = None ,
764
860
debug : Annotated [bool , typer .Option (help = "Enable debug logging" )] = False ,
765
861
):
766
862
"""Display the logs of a program
@@ -797,6 +893,9 @@ async def logs(
797
893
@app .command ()
798
894
async def runtime_checker (
799
895
item_hash : Annotated [str , typer .Argument (help = "Item hash of the runtime to check" )],
896
+ chain : Annotated [
897
+ Optional [Chain ], typer .Option (help = help_strings .ADDRESS_CHAIN , metavar = metavar_valid_chains )
898
+ ] = None ,
800
899
private_key : Annotated [Optional [str ], typer .Option (help = help_strings .PRIVATE_KEY )] = settings .PRIVATE_KEY_STRING ,
801
900
private_key_file : Annotated [
802
901
Optional [Path ], typer .Option (help = help_strings .PRIVATE_KEY_FILE )
@@ -826,6 +925,7 @@ async def runtime_checker(
826
925
skip_volume = True ,
827
926
skip_env_var = True ,
828
927
address = None ,
928
+ payment_chain = chain ,
829
929
channel = settings .DEFAULT_CHANNEL ,
830
930
private_key = private_key ,
831
931
private_key_file = private_key_file ,
0 commit comments