Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions packages/cli/src/commands/billing/cancel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export default class BillingCancel extends Command {
required: false,
description: "Product ID",
default: "compute",
options: ["compute"],
options: ["compute", "eigenai"],
env: "ECLOUD_PRODUCT_ID",
}),
force: Flags.boolean({
Expand All @@ -34,7 +34,7 @@ export default class BillingCancel extends Command {
// Check subscription status first
this.debug(`\nChecking subscription status for ${flags.product}...`);
const status = await billing.getStatus({
productId: flags.product as "compute",
productId: flags.product as "compute" | "eigenai",
});

// Check if there's an active subscription to cancel
Expand All @@ -58,7 +58,7 @@ export default class BillingCancel extends Command {
this.log(`\nCanceling subscription for ${flags.product}...`);

const result = await billing.cancel({
productId: flags.product as "compute",
productId: flags.product as "compute" | "eigenai",
});

// Handle response (defensive - should always be canceled at this point)
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/commands/billing/status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export default class BillingStatus extends Command {
required: false,
description: "Product ID",
default: "compute",
options: ["compute"],
options: ["compute", "eigenai"],
env: "ECLOUD_PRODUCT_ID",
}),
};
Expand All @@ -25,7 +25,7 @@ export default class BillingStatus extends Command {
const billing = await createBillingClient(flags);

const result = await billing.getStatus({
productId: flags.product as "compute",
productId: flags.product as "compute" | "eigenai",
});

const formatExpiry = (timestamp?: number) =>
Expand Down
213 changes: 161 additions & 52 deletions packages/cli/src/commands/billing/subscribe.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Command, Flags } from "@oclif/core";
import { isSubscriptionActive } from "@layr-labs/ecloud-sdk";
import type { ProductID, ChainID } from "@layr-labs/ecloud-sdk";
import { createBillingClient } from "../../client";
import { commonFlags } from "../../flags";
import chalk from "chalk";
Expand All @@ -19,9 +20,15 @@ export default class BillingSubscribe extends Command {
required: false,
description: "Product ID",
default: "compute",
options: ["compute"],
options: ["compute", "eigenai"],
env: "ECLOUD_PRODUCT_ID",
}),
"chain-id": Flags.string({
required: false,
description: "Chain ID for EigenAI subscription (required when product is eigenai)",
options: ["ethereum-mainnet", "ethereum-sepolia"],
env: "ECLOUD_CHAIN_ID",
}),
};

async run() {
Expand All @@ -31,69 +38,171 @@ export default class BillingSubscribe extends Command {

this.debug(`\nChecking subscription status for ${flags.product}...`);

const result = await billing.subscribe({
productId: flags.product as "compute",
});

// Handle already active subscription
if (result.type === "already_active") {
this.log(
`\n${chalk.green("✓")} Wallet ${chalk.bold(billing.address)} is already subscribed to ${flags.product}.`,
);
this.log(chalk.gray("Run 'ecloud billing status' for details."));
// Handle compute subscription
if (flags.product === "compute") {
await this.handleComputeSubscription(billing);
return;
}

// Handle payment issue
if (result.type === "payment_issue") {
this.log(
`\n${chalk.yellow("⚠")} You already have a subscription on ${flags.product}, but it has a payment issue.`,
);
this.log("Please update your payment method to restore access.");

if (result.portalUrl) {
this.log(`\n${chalk.bold("Update payment method:")}`);
this.log(` ${result.portalUrl}`);
// Handle eigenai subscription
if (flags.product === "eigenai") {
// Validate chain-id is provided for eigenai product
if (!flags["chain-id"]) {
this.error(
`${chalk.red("Error:")} --chain-id is required when subscribing to eigenai.\n` +
` Use: ecloud billing subscribe --product eigenai --chain-id ethereum-mainnet`,
);
}
await this.handleEigenAISubscription(billing, flags["chain-id"] as ChainID);
return;
}
});
}

// Open checkout URL in browser
this.log(`\nOpening checkout for wallet ${chalk.bold(billing.address)}...`);
this.log(chalk.gray(`\nURL: ${result.checkoutUrl}`));
await open(result.checkoutUrl);

// Poll for subscription status
this.log(`\n${chalk.gray("Waiting for payment confirmation...")}`);

const startTime = Date.now();

while (Date.now() - startTime < PAYMENT_TIMEOUT_MS) {
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));

try {
const status = await billing.getStatus({
productId: flags.product as "compute",
});

// Check if subscription is now active or trialing
if (isSubscriptionActive(status.subscriptionStatus)) {
this.log(
`\n${chalk.green("✓")} Subscription activated successfully for ${flags.product}!`,
);
this.log(`\n${chalk.gray("Start deploying with:")} ecloud compute app deploy`);
return;
}
} catch (error) {
this.debug(`Error polling for subscription status: ${error}`);
}
}
/**
* Handle subscription for compute product
*/
private async handleComputeSubscription(
billing: Awaited<ReturnType<typeof createBillingClient>>,
) {
const result = await billing.subscribe({
productId: "compute",
});

// Timeout reached
// Handle already active subscription
if (result.type === "already_active") {
this.log(
`\n${chalk.green("✓")} Wallet ${chalk.bold(billing.address)} is already subscribed to compute.`,
);
this.log(chalk.gray("Run 'ecloud billing status' for details."));
return;
}

// Handle payment issue
if (result.type === "payment_issue") {
this.log(
`\n${chalk.yellow("⚠")} You already have a subscription on compute, but it has a payment issue.`,
);
this.log("Please update your payment method to restore access.");

if (result.portalUrl) {
this.log(`\n${chalk.bold("Update payment method:")}`);
this.log(` ${result.portalUrl}`);
}
return;
}

// Open checkout URL in browser
this.log(`\nOpening checkout for wallet ${chalk.bold(billing.address)}...`);
this.log(chalk.gray(`\nURL: ${result.checkoutUrl}`));
await open(result.checkoutUrl);

// Poll for subscription status
this.log(`\n${chalk.gray("Waiting for payment confirmation...")}`);
const activated = await this.pollForSubscriptionStatus(billing, "compute");

if (activated) {
this.log(`\n${chalk.green("✓")} Subscription activated successfully for compute!`);
this.log(`\n${chalk.gray("Start deploying with:")} ecloud compute app deploy`);
} else {
this.log(`\n${chalk.yellow("⚠")} Payment confirmation timed out after 5 minutes.`);
this.log(
chalk.gray(`If you completed payment, run 'ecloud billing status' to check status.`),
);
}
}

/**
* Handle subscription for eigenai product
*/
private async handleEigenAISubscription(
billing: Awaited<ReturnType<typeof createBillingClient>>,
chainId: ChainID,
) {
const result = await billing.subscribeEigenAI({
productId: "eigenai",
chainId,
});

// Handle already active subscription
if (result.type === "already_active") {
this.log(
`\n${chalk.green("✓")} Wallet ${chalk.bold(billing.address)} is already subscribed to eigenai.`,
);
this.log(chalk.gray("Run 'ecloud billing status --product eigenai' for details."));
return;
}

// Handle payment issue
if (result.type === "payment_issue") {
this.log(
`\n${chalk.yellow("⚠")} You already have a subscription on eigenai, but it has a payment issue.`,
);
this.log("Please update your payment method to restore access.");

if (result.portalUrl) {
this.log(`\n${chalk.bold("Update payment method:")}`);
this.log(` ${result.portalUrl}`);
}
return;
}

// Display the API key prominently - this is shown only once!
this.log(`\n${chalk.bgYellow.black(" IMPORTANT ")} ${chalk.yellow("Save your API key now!")}`);
this.log(chalk.yellow("This key will only be shown once and cannot be recovered.\n"));
this.log(`${chalk.bold("Your EigenAI API Key:")}`);
this.log(` ${chalk.cyan(result.apiKey)}\n`);
this.log(chalk.gray("Store this key securely. You will need it to authenticate API requests."));

// Open checkout URL in browser
this.log(`\n${chalk.bold("Opening checkout for wallet")} ${chalk.bold(billing.address)}...`);
this.log(chalk.gray(`\nURL: ${result.checkoutUrl}`));
await open(result.checkoutUrl);

// Poll for subscription status
this.log(`\n${chalk.gray("Waiting for payment confirmation...")}`);
const activated = await this.pollForSubscriptionStatus(billing, "eigenai");

if (activated) {
this.log(`\n${chalk.green("✓")} Subscription activated successfully for eigenai!`);
this.log(
`\n${chalk.gray("Your EigenAI subscription is now active. Use your API key to make requests.")}`,
);
} else {
this.log(`\n${chalk.yellow("⚠")} Payment confirmation timed out after 5 minutes.`);
this.log(
chalk.gray(
`If you completed payment, run 'ecloud billing status --product eigenai' to check status.`,
),
);
}
}

/**
* Poll for subscription activation status
* @returns true if subscription became active, false if timed out
*/
private async pollForSubscriptionStatus(
billing: Awaited<ReturnType<typeof createBillingClient>>,
productId: ProductID,
): Promise<boolean> {
const startTime = Date.now();

while (Date.now() - startTime < PAYMENT_TIMEOUT_MS) {
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));

try {
const status = await billing.getStatus({ productId });

// Check if subscription is now active or trialing
if (isSubscriptionActive(status.subscriptionStatus)) {
return true;
}
} catch (error) {
this.debug(`Error polling for subscription status: ${error}`);
}
}

return false;
}
}
9 changes: 3 additions & 6 deletions packages/cli/src/commands/compute/app/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -494,12 +494,9 @@ async function fetchAvailableInstanceTypes(
rpcUrl,
environment: environmentConfig.name,
});
const userApiClient = new UserApiClient(
environmentConfig,
walletClient,
publicClient,
{ clientId: getClientId() },
);
const userApiClient = new UserApiClient(environmentConfig, walletClient, publicClient, {
clientId: getClientId(),
});

const skuList = await userApiClient.getSKUs();
if (skuList.skus.length === 0) {
Expand Down
13 changes: 6 additions & 7 deletions packages/cli/src/commands/compute/app/upgrade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,12 +288,9 @@ export default class AppUpgrade extends Command {
rpcUrl,
environment: environmentConfig.name,
});
const userApiClient = new UserApiClient(
environmentConfig,
walletClient,
publicClient,
{ clientId: getClientId() },
);
const userApiClient = new UserApiClient(environmentConfig, walletClient, publicClient, {
clientId: getClientId(),
});
const infos = await userApiClient.getInfos([appID], 1);
if (infos.length > 0) {
currentInstanceType = infos[0].machineType || "";
Expand Down Expand Up @@ -398,7 +395,9 @@ async function fetchAvailableInstanceTypes(
rpcUrl,
environment: environmentConfig.name,
});
const userApiClient = new UserApiClient(environmentConfig, walletClient, publicClient, { clientId: getClientId() });
const userApiClient = new UserApiClient(environmentConfig, walletClient, publicClient, {
clientId: getClientId(),
});

const skuList = await userApiClient.getSKUs();
if (skuList.skus.length === 0) {
Expand Down
12 changes: 9 additions & 3 deletions packages/cli/src/commands/compute/build/submit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,9 +111,13 @@ export default class BuildSubmit extends Command {
`\nUse ${chalk.yellow(`ecloud compute build logs ${buildId} --follow`)} to watch progress`,
);
this.log("");
this.log(chalk.gray("After the build completes, deploy or upgrade your app with the digest:"));
this.log(
chalk.gray("After the build completes, deploy or upgrade your app with the digest:"),
);
this.log(chalk.gray(" ecloud compute app deploy --verifiable --image-ref <digest>"));
this.log(chalk.gray(" ecloud compute app upgrade <app-id> --verifiable --image-ref <digest>"));
this.log(
chalk.gray(" ecloud compute app upgrade <app-id> --verifiable --image-ref <digest>"),
);
}
return;
}
Expand Down Expand Up @@ -182,7 +186,9 @@ export default class BuildSubmit extends Command {
this.log(` ecloud compute app deploy --verifiable --image-ref ${build.imageDigest}`);
this.log("");
this.log(chalk.yellow(" Upgrade an existing app:"));
this.log(` ecloud compute app upgrade <app-id> --verifiable --image-ref ${build.imageDigest}`);
this.log(
` ecloud compute app upgrade <app-id> --verifiable --image-ref ${build.imageDigest}`,
);
} else {
this.log(chalk.red(`Build failed: ${build.errorMessage ?? "Unknown error"}`));
}
Expand Down
9 changes: 3 additions & 6 deletions packages/cli/src/utils/appResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,12 +243,9 @@ export class AppResolver {
}

// Fetch info for all apps to get profile names
const userApiClient = new UserApiClient(
this.environmentConfig,
walletClient,
publicClient,
{ clientId: getClientId() },
);
const userApiClient = new UserApiClient(this.environmentConfig, walletClient, publicClient, {
clientId: getClientId(),
});
const appInfos = await getAppInfosChunked(userApiClient, apps);

// Build profile names map
Expand Down
9 changes: 3 additions & 6 deletions packages/cli/src/utils/prompts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1052,12 +1052,9 @@ async function getAppIDInteractive(options: GetAppIDOptions): Promise<Address> {
// If cache is empty/expired, fetch fresh profile names from API
if (!cachedProfiles) {
try {
const userApiClient = new UserApiClient(
environmentConfig,
walletClient,
publicClient,
{ clientId: getClientId() },
);
const userApiClient = new UserApiClient(environmentConfig, walletClient, publicClient, {
clientId: getClientId(),
});
const appInfos = await getAppInfosChunked(userApiClient, apps);

// Build and cache profile names
Expand Down
Loading