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
29 changes: 28 additions & 1 deletion packages/cli/src/commands/billing/__tests__/status.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ vi.mock("../../../utils/viemClients", () => ({
import { createBillingClient } from "../../../client";
import { createViemClients } from "../../../utils/viemClients";

describe("ecloud billing status — top-up hint", () => {
describe("ecloud billing status", () => {
let logOutput: string[];
let mockBilling: {
address: string;
Expand Down Expand Up @@ -104,6 +104,33 @@ describe("ecloud billing status — top-up hint", () => {
expect(fullOutput).not.toContain("Need more credits?");
});

describe("credit expiry", () => {
it("does not render the API's string zero sentinel as the Unix epoch", async () => {
const output = await runStatusCommand({
subscriptionStatus: "active",
productId: "compute",
remainingCredits: 5,
nextCreditExpiry: "0",
});
const fullOutput = output.join("\n");

expect(fullOutput).toContain("Credits: $5.00");
expect(fullOutput).not.toContain("expires");
expect(fullOutput).not.toContain("1970");
});

it("renders a valid positive expiry timestamp", async () => {
const output = await runStatusCommand({
subscriptionStatus: "active",
productId: "compute",
remainingCredits: 5,
nextCreditExpiry: 1893456000,
});

expect(output.join("\n")).toContain("expires");
});
});

describe("wallet ETH balance line", () => {
it("warns (does not silently swallow) when the balance read fails, but still completes", async () => {
(createViemClients as ReturnType<typeof vi.fn>).mockImplementation(() => {
Expand Down
10 changes: 8 additions & 2 deletions packages/cli/src/commands/billing/status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,14 @@ export default class BillingStatus extends Command {
productId: flags.product as "compute",
});

const formatExpiry = (timestamp?: number) =>
timestamp ? ` (expires ${new Date(timestamp * 1000).toLocaleDateString()})` : "";
const formatExpiry = (timestamp?: number | string | null) => {
const normalizedTimestamp = Number(timestamp);
if (!Number.isFinite(normalizedTimestamp) || normalizedTimestamp <= 0) {
return "";
}

return ` (expires ${new Date(normalizedTimestamp * 1000).toLocaleDateString()})`;
};

// Format status with appropriate color and symbol
const formatStatus = (status: string) => {
Expand Down
9 changes: 6 additions & 3 deletions packages/sdk/src/client/modules/compute/app/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -505,10 +505,11 @@ async function checkQuotaAvailable(
throw new Error(`failed to get quota limit: ${err.message}`);
}

// If quota is 0, user needs to subscribe
// A zero quota means deployment access has not been initialized. Billing is
// credit-based, so subscribing is not an appropriate recovery action.
if (maxQuota === 0) {
throw new Error(
"no app quota available. Run 'ecloud billing subscribe' to enable app deployment",
`no app quota available for ${environmentConfig.name}. If you recently purchased compute credits, quota initialization may still be pending. Retry shortly. If the issue persists, contact EigenCloud support at eigencloud_support@eigenlabs.org and include wallet ${userAddress}.`,
);
}

Expand Down Expand Up @@ -591,7 +592,9 @@ export async function prepareDeploy(

// 4. Generate or use provided salt
const salt = options.salt ?? generateRandomSalt();
logger.debug(`${options.salt ? "Using provided" : "Generated"} salt: ${Buffer.from(salt).toString("hex")}`);
logger.debug(
`${options.salt ? "Using provided" : "Generated"} salt: ${Buffer.from(salt).toString("hex")}`,
);

// 5. Get app ID (calculate from salt and address)
logger.debug("Calculating app ID...");
Expand Down