Skip to content

Commit fbae1c4

Browse files
authored
Feat: New pricing system (#332)
- Add/use new price aggregate - New compute units system - Display selection table and checkout for each entity - Add/use new cost endpoint - Split flows (between 80/20) - Handle legacy flow/VM older than wallet_community_timestamp - Fetch CRN list from new program endpoint - Select an available GPU and then filter CRNs - Display GPU pricing and compute units selection - `internet` field not passed on program creation. It's an important fix since internet=True doubles the cost - Display program pricing - New `aleph pricing <entity>` cmd + tests - New `aleph instance gpu` cmd - Deprecate and remove firecracker hypervisor for instances - Upgrade CI action versions
1 parent 5669846 commit fbae1c4

File tree

22 files changed

+1486
-678
lines changed

22 files changed

+1486
-678
lines changed

.github/workflows/pytest.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ jobs:
4141
4242
- name: Set up Python for macOS
4343
if: startsWith(matrix.os, 'macos')
44-
uses: actions/setup-python@v2
44+
uses: actions/setup-python@v5
4545
with:
4646
python-version: 3.11
4747

.github/workflows/test-build.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ jobs:
2020
runs-on: ${{matrix.os}}
2121

2222
steps:
23-
- uses: actions/checkout@v2
23+
- uses: actions/checkout@v4
2424

2525
- name: Workaround github issue https://github.com/actions/runner-images/issues/7192
2626
if: startsWith(matrix.os, 'ubuntu-')
@@ -35,7 +35,7 @@ jobs:
3535
3636
- name: Set up Python for macOS
3737
if: startsWith(matrix.os, 'macos')
38-
uses: actions/setup-python@v2
38+
uses: actions/setup-python@v5
3939
with:
4040
python-version: 3.11
4141

.github/workflows/test-docker.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ jobs:
1616
runs-on: ubuntu-22.04
1717

1818
steps:
19-
- uses: actions/checkout@v2
19+
- uses: actions/checkout@v4
2020

2121
# Use GitHub's Docker registry to cache intermediate layers
2222
- run: echo ${{ secrets.GITHUB_TOKEN }} | docker login docker.pkg.github.com

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ dynamic = [ "version" ]
3030
dependencies = [
3131
"aiodns==3.2",
3232
"aiohttp==3.11.12",
33-
"aleph-message>=0.6",
34-
"aleph-sdk-python>=1.3,<2",
33+
"aleph-message>=0.6.1",
34+
"aleph-sdk-python>=1.4,<2",
3535
"base58==2.1.1", # Needed now as default with _load_account changement
3636
"py-sr25519-bindings==0.2", # Needed for DOT signatures
3737
"pygments==2.19.1",

src/aleph_client/__main__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
instance,
1212
message,
1313
node,
14+
pricing,
1415
program,
1516
)
1617
from aleph_client.utils import AsyncTyper
@@ -32,6 +33,7 @@
3233
app.add_typer(domain.app, name="domain", help="Manage custom domain (DNS) on aleph.im & twentysix.cloud")
3334
app.add_typer(node.app, name="node", help="Get node info on aleph.im & twentysix.cloud")
3435
app.add_typer(about.app, name="about", help="Display the informations of Aleph CLI")
36+
app.command("pricing")(pricing.prices_for_service)
3537

3638
if __name__ == "__main__":
3739
app()

src/aleph_client/commands/account.py

Lines changed: 53 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
get_chains_with_super_token,
2222
get_compatible_chains,
2323
)
24-
from aleph.sdk.utils import bytes_from_hex
24+
from aleph.sdk.utils import bytes_from_hex, displayable_amount
2525
from aleph_message.models import Chain
2626
from rich.console import Console
2727
from rich.panel import Panel
@@ -241,6 +241,20 @@ def sign_bytes(
241241
typer.echo("\nSignature: " + signature.hex())
242242

243243

244+
async def get_balance(address: str) -> dict:
245+
balance_data: dict = {}
246+
uri = f"{settings.API_HOST}/api/v0/addresses/{address}/balance"
247+
async with aiohttp.ClientSession() as session:
248+
response = await session.get(uri)
249+
if response.status == 200:
250+
balance_data = await response.json()
251+
balance_data["available_amount"] = balance_data["balance"] - balance_data["locked_amount"]
252+
else:
253+
error = f"Failed to retrieve balance for address {address}. Status code: {response.status}"
254+
raise Exception(error)
255+
return balance_data
256+
257+
244258
@app.command()
245259
async def balance(
246260
address: Optional[str] = typer.Option(None, help="Address"),
@@ -255,54 +269,46 @@ async def balance(
255269
address = account.get_address()
256270

257271
if address:
258-
uri = f"{settings.API_HOST}/api/v0/addresses/{address}/balance"
259-
260-
async with aiohttp.ClientSession() as session:
261-
response = await session.get(uri)
262-
if response.status == 200:
263-
balance_data = await response.json()
264-
balance_data["available_amount"] = balance_data["balance"] - balance_data["locked_amount"]
265-
266-
infos = [
267-
Text.from_markup(f"Address: [bright_cyan]{balance_data['address']}[/bright_cyan]"),
268-
Text.from_markup(
269-
f"\nBalance: [bright_cyan]{balance_data['balance']:.2f}".rstrip("0").rstrip(".")
270-
+ "[/bright_cyan]"
271-
),
272-
]
273-
details = balance_data.get("details")
274-
if details:
275-
infos += [Text("\n ↳ Details")]
276-
for chain_, chain_balance in details.items():
277-
infos += [
278-
Text.from_markup(
279-
f"\n {chain_}: [orange3]{chain_balance:.2f}".rstrip("0").rstrip(".") + "[/orange3]"
280-
)
281-
]
282-
available_color = "bright_cyan" if balance_data["available_amount"] >= 0 else "red"
283-
infos += [
284-
Text.from_markup(
285-
f"\n - Locked: [bright_cyan]{balance_data['locked_amount']:.2f}".rstrip("0").rstrip(".")
286-
+ "[/bright_cyan]"
287-
),
288-
Text.from_markup(
289-
f"\n - Available: [{available_color}]{balance_data['available_amount']:.2f}".rstrip("0").rstrip(
290-
"."
272+
try:
273+
balance_data = await get_balance(address)
274+
infos = [
275+
Text.from_markup(f"Address: [bright_cyan]{balance_data['address']}[/bright_cyan]"),
276+
Text.from_markup(
277+
f"\nBalance: [bright_cyan]{displayable_amount(balance_data['balance'], decimals=2)}[/bright_cyan]"
278+
),
279+
]
280+
details = balance_data.get("details")
281+
if details:
282+
infos += [Text("\n ↳ Details")]
283+
for chain_, chain_balance in details.items():
284+
infos += [
285+
Text.from_markup(
286+
f"\n {chain_}: [orange3]{displayable_amount(chain_balance, decimals=2)}[/orange3]"
291287
)
292-
+ f"[/{available_color}]"
293-
),
294-
]
295-
console.print(
296-
Panel(
297-
Text.assemble(*infos),
298-
title="Account Infos",
299-
border_style="bright_cyan",
300-
expand=False,
301-
title_align="left",
302-
)
288+
]
289+
available_color = "bright_cyan" if balance_data["available_amount"] >= 0 else "red"
290+
infos += [
291+
Text.from_markup(
292+
f"\n - Locked: [bright_cyan]{displayable_amount(balance_data['locked_amount'], decimals=2)}"
293+
"[/bright_cyan]"
294+
),
295+
Text.from_markup(
296+
f"\n - Available: [{available_color}]"
297+
f"{displayable_amount(balance_data['available_amount'], decimals=2)}"
298+
f"[/{available_color}]"
299+
),
300+
]
301+
console.print(
302+
Panel(
303+
Text.assemble(*infos),
304+
title="Account Infos",
305+
border_style="bright_cyan",
306+
expand=False,
307+
title_align="left",
303308
)
304-
else:
305-
typer.echo(f"Failed to retrieve balance for address {address}. Status code: {response.status}")
309+
)
310+
except Exception as e:
311+
typer.echo(e)
306312
else:
307313
typer.echo("Error: Please provide either a private key, private key file, or an address.")
308314

src/aleph_client/commands/files.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,9 @@ async def download(
142142

143143
@app.command()
144144
async def forget(
145-
item_hash: str = typer.Argument(..., help="Hash to forget"),
145+
item_hash: str = typer.Argument(
146+
..., help="Hash(es) to forget. Must be a comma separated list. Example: `123...abc` or `123...abc,456...xyz`"
147+
),
146148
reason: str = typer.Argument("User deletion", help="reason to forget"),
147149
channel: Optional[str] = typer.Option(default=settings.DEFAULT_CHANNEL, help=help_strings.CHANNEL),
148150
private_key: Optional[str] = typer.Option(settings.PRIVATE_KEY_STRING, help=help_strings.PRIVATE_KEY),
@@ -155,8 +157,10 @@ async def forget(
155157

156158
account: AccountFromPrivateKey = _load_account(private_key, private_key_file)
157159

160+
hashes = [ItemHash(item_hash) for item_hash in item_hash.split(",")]
161+
158162
async with AuthenticatedAlephHttpClient(account=account, api_server=settings.API_HOST) as client:
159-
value = await client.forget(hashes=[ItemHash(item_hash)], reason=reason, channel=channel)
163+
value = await client.forget(hashes=hashes, reason=reason, channel=channel)
160164
typer.echo(f"{value[0].json(indent=4)}")
161165

162166

src/aleph_client/commands/help_strings.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,15 @@
1717
ASK_FOR_CONFIRMATION = "Prompt user for confirmation"
1818
IPFS_CATCH_ALL_PATH = "Choose a relative path to catch all unmatched route or a 404 error"
1919
PAYMENT_TYPE = "Payment method, either holding tokens, NFTs, or Pay-As-You-Go via token streaming"
20-
HYPERVISOR = "Hypervisor to use to launch your instance. Defaults to QEMU"
20+
HYPERVISOR = "Hypervisor to use to launch your instance. Always defaults to QEMU, since Firecracker is now deprecated for instances"
2121
INSTANCE_NAME = "Name of your new instance"
2222
ROOTFS = (
2323
"Hash of the rootfs to use for your instance. Defaults to Ubuntu 22. You can also create your own rootfs and pin it"
2424
)
25-
ROOTFS_SIZE = (
26-
"Size of the rootfs to use for your instance. If not set, content.size of the --rootfs store message will be used"
27-
)
25+
COMPUTE_UNITS = "Number of compute units to allocate. Compute units correspond to a tier that includes vcpus, memory, disk and gpu presets. For reference, run: `aleph pricing --help`"
26+
ROOTFS_SIZE = "Rootfs size in MiB to allocate"
2827
VCPUS = "Number of virtual CPUs to allocate"
29-
MEMORY = "Maximum memory (RAM) allocation on VM in MiB"
28+
MEMORY = "Maximum memory (RAM) in MiB to allocate"
3029
TIMEOUT_SECONDS = "If vm is not called after [timeout_seconds] it will shutdown"
3130
SSH_PUBKEY_FILE = "Path to a public ssh key to be added to the instance"
3231
CRN_HASH = "Hash of the CRN to deploy to (only applicable for confidential and/or Pay-As-You-Go instances)"
@@ -37,6 +36,7 @@
3736
CONFIDENTIAL_FIRMWARE_HASH = "Hash of the UEFI Firmware content, to validate measure (ignored if path is provided)"
3837
CONFIDENTIAL_FIRMWARE_PATH = "Path to the UEFI Firmware content, to validate measure (instead of the hash)"
3938
GPU_OPTION = "Launch an instance attaching a GPU to it"
39+
GPU_PREMIUM_OPTION = "Use Premium GPUs (VRAM > 48GiB)"
4040
KEEP_SESSION = "Keeping the already initiated session"
4141
VM_SECRET = "Secret password to start the VM"
4242
CRN_URL_VM_DELETION = "Domain of the CRN where an associated VM is running. It ensures your VM will be stopped and erased on the CRN before the instance message is actually deleted"
@@ -51,15 +51,18 @@
5151
PAYMENT_CHAIN_USED = "Chain you are using to pay for your instance"
5252
ORIGIN_CHAIN = "Chain of origin of your private key (ensuring correct parsing)"
5353
ADDRESS_CHAIN = "Chain for the address"
54+
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/"
5455
CREATE_REPLACE = "Overwrites private key file if it already exists"
5556
CREATE_ACTIVE = "Loads the new private key after creation"
5657
PROMPT_CRN_URL = "URL of the CRN (Compute node) on which the instance is running"
5758
PROMPT_PROGRAM_CRN_URL = "URL of the CRN (Compute node) on which the program is running"
5859
PROGRAM_PATH = "Path to your source code. Can be a directory, a .squashfs file or a .zip archive"
5960
PROGRAM_ENTRYPOINT = "Your program entrypoint. Example: `main:app` for Python programs, else `run.sh` for a script containing your launch command"
6061
PROGRAM_RUNTIME = "Hash of the runtime to use for your program. You can also create your own runtime and pin it. Currently defaults to `{runtime_id}` (Use `aleph program runtime-checker` to inspect it)"
61-
PROGRAM_BETA = "If true, you will be prompted to add message subscriptions to your program"
62+
PROGRAM_INTERNET = "Enable internet access for your program. By default, internet access is disabled"
63+
PROGRAM_PERSISTENT = "Create your program as persistent. By default, programs are ephemeral (serverless): they only start when called and then shutdown after the defined timeout delay."
6264
PROGRAM_UPDATABLE = "Allow program updates. By default, only the source code can be modified without requiring redeployement (same item hash). When enabled (set to True), this option allows to update any other field. However, such modifications will require a program redeployment (new item hash)"
65+
PROGRAM_BETA = "If true, you will be prompted to add message subscriptions to your program"
6366
PROGRAM_KEEP_CODE = "Keep the source code intact instead of deleting it"
6467
PROGRAM_KEEP_PREV = "Keep the previous program intact instead of deleting it"
6568
TARGET_ADDRESS = "Target address. Defaults to current account address"

0 commit comments

Comments
 (0)