Skip to content

Commit e770682

Browse files
authored
Fix program chain and UX (#343)
- Display pricing for each program in list - Fix payment chain -> SOL wasn't working properly - Simplify tests after Annotated update
1 parent effa2ea commit e770682

File tree

4 files changed

+139
-88
lines changed

4 files changed

+139
-88
lines changed

pyproject.toml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,15 @@ dependencies = [
3131
"aiodns==3.2",
3232
"aiohttp==3.11.12",
3333
"aleph-message>=0.6.1",
34-
"aleph-sdk-python>=1.4,<2",
35-
"base58==2.1.1", # Needed now as default with _load_account changement
36-
"py-sr25519-bindings==0.2", # Needed for DOT signatures
34+
"aleph-sdk-python @ git+https://github.com/aleph-im/aleph-sdk-python",
35+
"base58==2.1.1", # Needed now as default with _load_account changement
36+
"py-sr25519-bindings==0.2", # Needed for DOT signatures
3737
"pygments==2.19.1",
38-
"pynacl==1.5", # Needed now as default with _load_account changement
38+
"pynacl==1.5", # Needed now as default with _load_account changement
3939
"python-magic==0.4.27",
4040
"rich==13.9.*",
4141
"setuptools>=65.5",
42-
"substrate-interface==1.7.11", # Needed for DOT signatures
42+
"substrate-interface==1.7.11", # Needed for DOT signatures
4343
"textual==0.73",
4444
"typer==0.15.1",
4545
]

src/aleph_client/commands/help_strings.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@
4949
ALLOCATION_MANUAL = "Manual - Selection"
5050
PAYMENT_CHAIN = "Chain you want to use to pay for your instance"
5151
PAYMENT_CHAIN_USED = "Chain you are using to pay for your instance"
52+
PAYMENT_CHAIN_PROGRAM = "Chain you want to use to pay for your program"
53+
PAYMENT_CHAIN_PROGRAM_USED = "Chain you are using to pay for your program"
5254
ORIGIN_CHAIN = "Chain of origin of your private key (ensuring correct parsing)"
5355
ADDRESS_CHAIN = "Chain for the address"
5456
ADDRESS_PAYER = "Address of the payer. In order to delegate the payment, your account must be authorized beforehand to publish on the behalf of this address. See the docs for more info: https://docs.aleph.im/protocol/permissions/"

src/aleph_client/commands/program.py

Lines changed: 118 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,16 @@
77
from collections.abc import Mapping
88
from decimal import Decimal
99
from pathlib import Path
10-
from typing import Annotated, Any, Optional, cast
10+
from typing import Annotated, Any, Optional, Union, cast
1111
from zipfile import BadZipFile
1212

1313
import aiohttp
1414
import typer
1515
from aleph.sdk import AlephHttpClient, AuthenticatedAlephHttpClient
1616
from aleph.sdk.account import _load_account
1717
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
1920
from aleph.sdk.exceptions import (
2021
ForgottenMessageError,
2122
InsufficientFundsError,
@@ -24,8 +25,15 @@
2425
from aleph.sdk.query.filters import MessageFilter
2526
from aleph.sdk.query.responses import PriceResponse
2627
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+
)
2937
from aleph_message.models.execution.program import ProgramContent
3038
from aleph_message.models.item_hash import ItemHash
3139
from aleph_message.status import MessageStatus
@@ -55,6 +63,9 @@
5563
logger = logging.getLogger(__name__)
5664
app = AsyncTyper(no_args_is_help=True)
5765

66+
hold_chains = [*get_chains_with_holding(), Chain.SOL]
67+
metavar_valid_chains = f"[{'|'.join(hold_chains)}]"
68+
5869

5970
@app.command(name="upload")
6071
@app.command(name="create")
@@ -80,6 +91,14 @@ async def upload(
8091
skip_env_var: Annotated[bool, typer.Option(help=help_strings.SKIP_ENV_VAR)] = False,
8192
env_vars: Annotated[Optional[str], typer.Option(help=help_strings.ENVIRONMENT_VARIABLES)] = None,
8293
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,
83102
channel: Annotated[Optional[str], typer.Option(help=help_strings.CHANNEL)] = settings.DEFAULT_CHANNEL,
84103
private_key: Annotated[Optional[str], typer.Option(help=help_strings.PRIVATE_KEY)] = settings.PRIVATE_KEY_STRING,
85104
private_key_file: Annotated[
@@ -96,7 +115,7 @@ async def upload(
96115
For more information, see https://docs.aleph.im/computing"""
97116

98117
setup_logging(debug)
99-
118+
console = Console()
100119
path = path.absolute()
101120

102121
try:
@@ -108,9 +127,25 @@ async def upload(
108127
typer.echo("No such file or directory")
109128
raise typer.Exit(code=4) from error
110129

111-
account: AccountFromPrivateKey = _load_account(private_key, private_key_file)
130+
account: AccountFromPrivateKey = _load_account(private_key, private_key_file, chain=payment_chain)
112131
address = address or settings.ADDRESS_TO_USE or account.get_address()
113132

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+
114149
async with AuthenticatedAlephHttpClient(account=account, api_server=settings.API_HOST) as client:
115150
# Upload the source code
116151
with open(path_object, "rb") as fd:
@@ -181,6 +216,7 @@ async def upload(
181216
"runtime": runtime,
182217
"metadata": {"name": name},
183218
"address": address,
219+
"payment": payment,
184220
"vcpus": vcpus,
185221
"memory": memory,
186222
"timeout_seconds": timeout_seconds,
@@ -235,8 +271,6 @@ async def upload(
235271
hash_base32 = b32encode(b16decode(item_hash.upper())).strip(b"=").lower().decode()
236272
func_url_1 = f"{settings.VM_URL_PATH.format(hash=item_hash)}"
237273
func_url_2 = f"{settings.VM_URL_HOST.format(hash_base32=hash_base32)}"
238-
239-
console = Console()
240274
infos = [
241275
Text.from_markup(f"Your program [bright_cyan]{item_hash}[/bright_cyan] has been uploaded on aleph.im."),
242276
Text.assemble(
@@ -271,6 +305,9 @@ async def upload(
271305
async def update(
272306
item_hash: Annotated[str, typer.Argument(help="Item hash to update")],
273307
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,
274311
private_key: Annotated[Optional[str], typer.Option(help=help_strings.PRIVATE_KEY)] = settings.PRIVATE_KEY_STRING,
275312
private_key_file: Annotated[
276313
Optional[Path], typer.Option(help=help_strings.PRIVATE_KEY_FILE)
@@ -294,7 +331,7 @@ async def update(
294331
typer.echo("No such file or directory")
295332
raise typer.Exit(code=4) from error
296333

297-
account: AccountFromPrivateKey = _load_account(private_key, private_key_file)
334+
account: AccountFromPrivateKey = _load_account(private_key, private_key_file, chain=chain)
298335

299336
async with AuthenticatedAlephHttpClient(account=account, api_server=settings.API_HOST) as client:
300337
try:
@@ -381,6 +418,9 @@ async def delete(
381418
item_hash: Annotated[str, typer.Argument(help="Item hash to unpersist")],
382419
reason: Annotated[str, typer.Option(help="Reason for deleting the program")] = "User deletion",
383420
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,
384424
private_key: Annotated[Optional[str], typer.Option(help=help_strings.PRIVATE_KEY)] = settings.PRIVATE_KEY_STRING,
385425
private_key_file: Annotated[
386426
Optional[Path], typer.Option(help=help_strings.PRIVATE_KEY_FILE)
@@ -393,7 +433,7 @@ async def delete(
393433

394434
setup_logging(debug)
395435

396-
account = _load_account(private_key, private_key_file)
436+
account = _load_account(private_key, private_key_file, chain=chain)
397437

398438
async with AuthenticatedAlephHttpClient(account=account, api_server=settings.API_HOST) as client:
399439
try:
@@ -440,6 +480,9 @@ async def delete(
440480
@app.command(name="list")
441481
async def list_programs(
442482
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,
443486
private_key: Annotated[Optional[str], typer.Option(help=help_strings.PRIVATE_KEY)] = settings.PRIVATE_KEY_STRING,
444487
private_key_file: Annotated[
445488
Optional[Path], typer.Option(help=help_strings.PRIVATE_KEY_FILE)
@@ -451,7 +494,7 @@ async def list_programs(
451494

452495
setup_logging(debug)
453496

454-
account = _load_account(private_key, private_key_file)
497+
account = _load_account(private_key, private_key_file, chain=chain)
455498
address = address or settings.ADDRESS_TO_USE or account.get_address()
456499

457500
async with AlephHttpClient(api_server=settings.API_HOST) as client:
@@ -493,29 +536,74 @@ async def list_programs(
493536
)
494537
msg_link = f"https://explorer.aleph.im/address/ETH/{message.sender}/message/PROGRAM/{message.item_hash}"
495538
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"))
496559
created_at = Text.assemble(
497-
"URLs ↓\t Created at: ",
560+
"Created at: ",
498561
Text(
499562
str(str_to_datetime(str(safe_getattr(message, "content.time")))).split(".", maxsplit=1)[0],
500563
style="orchid",
501564
),
502565
)
566+
payer: Union[str, Text] = ""
567+
if message.sender != message.content.address:
568+
payer = Text.assemble("\nPayer: ", 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)
503579
hash_base32 = b32encode(b16decode(message.item_hash.upper())).strip(b"=").lower().decode()
504580
func_url_1 = settings.VM_URL_PATH.format(hash=message.item_hash)
505581
func_url_2 = settings.VM_URL_HOST.format(hash_base32=hash_base32)
506582
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]"
508585
)
509586
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,
511600
)
512601
specs = [
513602
f"vCPU: [magenta3]{message.content.resources.vcpus}[/magenta3]\n",
514603
f"RAM: [magenta3]{message.content.resources.memory / 1_024:.2f} GiB[/magenta3]\n",
515604
"HyperV: [magenta3]Firecracker[/magenta3]\n",
516605
f"Timeout: [orange3]{message.content.resources.seconds}s[/orange3]\n",
517606
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",
519607
f"Updatable: {'[green]Yes[/green]' if message.content.allow_amend else '[orange3]Code only[/orange3]'}",
520608
]
521609
specifications = Text.from_markup("".join(specs))
@@ -568,6 +656,9 @@ async def list_programs(
568656
async def persist(
569657
item_hash: Annotated[str, typer.Argument(help="Item hash to persist")],
570658
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,
571662
private_key: Annotated[Optional[str], typer.Option(help=help_strings.PRIVATE_KEY)] = settings.PRIVATE_KEY_STRING,
572663
private_key_file: Annotated[
573664
Optional[Path], typer.Option(help=help_strings.PRIVATE_KEY_FILE)
@@ -582,7 +673,7 @@ async def persist(
582673

583674
setup_logging(debug)
584675

585-
account = _load_account(private_key, private_key_file)
676+
account = _load_account(private_key, private_key_file, chain=chain)
586677

587678
async with AuthenticatedAlephHttpClient(account=account, api_server=settings.API_HOST) as client:
588679
try:
@@ -662,6 +753,9 @@ async def persist(
662753
async def unpersist(
663754
item_hash: Annotated[str, typer.Argument(help="Item hash to unpersist")],
664755
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,
665759
private_key: Annotated[Optional[str], typer.Option(help=help_strings.PRIVATE_KEY)] = settings.PRIVATE_KEY_STRING,
666760
private_key_file: Annotated[
667761
Optional[Path], typer.Option(help=help_strings.PRIVATE_KEY_FILE)
@@ -676,7 +770,7 @@ async def unpersist(
676770

677771
setup_logging(debug)
678772

679-
account = _load_account(private_key, private_key_file)
773+
account = _load_account(private_key, private_key_file, chain=chain)
680774

681775
async with AuthenticatedAlephHttpClient(account=account, api_server=settings.API_HOST) as client:
682776
try:
@@ -755,12 +849,14 @@ async def unpersist(
755849
@app.command()
756850
async def logs(
757851
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,
758855
private_key: Annotated[Optional[str], typer.Option(help=help_strings.PRIVATE_KEY)] = settings.PRIVATE_KEY_STRING,
759856
private_key_file: Annotated[
760857
Optional[Path], typer.Option(help=help_strings.PRIVATE_KEY_FILE)
761858
] = settings.PRIVATE_KEY_FILE,
762859
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,
764860
debug: Annotated[bool, typer.Option(help="Enable debug logging")] = False,
765861
):
766862
"""Display the logs of a program
@@ -797,6 +893,9 @@ async def logs(
797893
@app.command()
798894
async def runtime_checker(
799895
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,
800899
private_key: Annotated[Optional[str], typer.Option(help=help_strings.PRIVATE_KEY)] = settings.PRIVATE_KEY_STRING,
801900
private_key_file: Annotated[
802901
Optional[Path], typer.Option(help=help_strings.PRIVATE_KEY_FILE)
@@ -826,6 +925,7 @@ async def runtime_checker(
826925
skip_volume=True,
827926
skip_env_var=True,
828927
address=None,
928+
payment_chain=chain,
829929
channel=settings.DEFAULT_CHANNEL,
830930
private_key=private_key,
831931
private_key_file=private_key_file,

0 commit comments

Comments
 (0)