@layr-labs/ecloud-cli 0.4.0-dev → 0.4.0-dev.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/LICENSE +7 -0
  2. package/bin/dev.js +0 -0
  3. package/dist/commands/auth/generate.js +0 -0
  4. package/dist/commands/auth/login.js +0 -0
  5. package/dist/commands/auth/logout.js +0 -0
  6. package/dist/commands/auth/migrate.js +0 -0
  7. package/dist/commands/auth/whoami.js +0 -0
  8. package/dist/commands/billing/__tests__/status.test.js +552 -0
  9. package/dist/commands/billing/__tests__/status.test.js.map +1 -0
  10. package/dist/commands/billing/__tests__/subscribe.test.js +580 -0
  11. package/dist/commands/billing/__tests__/subscribe.test.js.map +1 -0
  12. package/dist/commands/billing/__tests__/top-up.test.js +729 -0
  13. package/dist/commands/billing/__tests__/top-up.test.js.map +1 -0
  14. package/dist/commands/billing/cancel.js +0 -0
  15. package/dist/commands/billing/status.js +5 -0
  16. package/dist/commands/billing/status.js.map +1 -1
  17. package/dist/commands/billing/subscribe.js +30 -2
  18. package/dist/commands/billing/subscribe.js.map +1 -1
  19. package/dist/commands/billing/top-up.js +491 -0
  20. package/dist/commands/billing/top-up.js.map +1 -0
  21. package/dist/commands/compute/app/configure/tls.js +0 -0
  22. package/dist/commands/compute/app/create.js +0 -0
  23. package/dist/commands/compute/app/deploy.js +13 -3
  24. package/dist/commands/compute/app/deploy.js.map +1 -1
  25. package/dist/commands/compute/app/info.js +1 -1
  26. package/dist/commands/compute/app/info.js.map +1 -1
  27. package/dist/commands/compute/app/list.js +1 -1
  28. package/dist/commands/compute/app/list.js.map +1 -1
  29. package/dist/commands/compute/app/logs.js +1 -1
  30. package/dist/commands/compute/app/logs.js.map +1 -1
  31. package/dist/commands/compute/app/profile/set.js +1 -1
  32. package/dist/commands/compute/app/profile/set.js.map +1 -1
  33. package/dist/commands/compute/app/releases.js +1 -1
  34. package/dist/commands/compute/app/releases.js.map +1 -1
  35. package/dist/commands/compute/app/start.js +1 -1
  36. package/dist/commands/compute/app/start.js.map +1 -1
  37. package/dist/commands/compute/app/stop.js +1 -1
  38. package/dist/commands/compute/app/stop.js.map +1 -1
  39. package/dist/commands/compute/app/terminate.js +1 -1
  40. package/dist/commands/compute/app/terminate.js.map +1 -1
  41. package/dist/commands/compute/app/upgrade.js +1 -1
  42. package/dist/commands/compute/app/upgrade.js.map +1 -1
  43. package/dist/commands/compute/build/info.js +1 -1
  44. package/dist/commands/compute/build/info.js.map +1 -1
  45. package/dist/commands/compute/build/list.js +1 -1
  46. package/dist/commands/compute/build/list.js.map +1 -1
  47. package/dist/commands/compute/build/logs.js +1 -1
  48. package/dist/commands/compute/build/logs.js.map +1 -1
  49. package/dist/commands/compute/build/status.js +1 -1
  50. package/dist/commands/compute/build/status.js.map +1 -1
  51. package/dist/commands/compute/build/submit.js +1 -1
  52. package/dist/commands/compute/build/submit.js.map +1 -1
  53. package/dist/commands/compute/build/verify.js +1 -1
  54. package/dist/commands/compute/build/verify.js.map +1 -1
  55. package/dist/commands/compute/environment/list.js +0 -0
  56. package/dist/commands/compute/environment/set.js +0 -0
  57. package/dist/commands/compute/environment/show.js +0 -0
  58. package/dist/commands/compute/undelegate.js +1 -1
  59. package/dist/commands/compute/undelegate.js.map +1 -1
  60. package/dist/commands/telemetry/disable.js +0 -0
  61. package/dist/commands/telemetry/enable.js +0 -0
  62. package/dist/commands/telemetry/status.js +0 -0
  63. package/dist/commands/upgrade.js +0 -0
  64. package/dist/commands/version.js +0 -0
  65. package/package.json +14 -14
  66. package/VERSION +0 -2
@@ -0,0 +1,729 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/commands/billing/__tests__/top-up.test.ts
4
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
5
+
6
+ // src/commands/billing/top-up.ts
7
+ import { Command, Flags as Flags2 } from "@oclif/core";
8
+
9
+ // src/client.ts
10
+ import {
11
+ createComputeModule,
12
+ createBillingModule,
13
+ createBuildModule,
14
+ getEnvironmentConfig as getEnvironmentConfig3,
15
+ requirePrivateKey,
16
+ getPrivateKeyWithSource,
17
+ addHexPrefix as addHexPrefix2
18
+ } from "@layr-labs/ecloud-sdk";
19
+
20
+ // src/flags.ts
21
+ import { Flags } from "@oclif/core";
22
+ import { getBuildType as getBuildType2 } from "@layr-labs/ecloud-sdk";
23
+
24
+ // src/utils/prompts.ts
25
+ import { input, select, password, confirm as inquirerConfirm } from "@inquirer/prompts";
26
+ import chalk from "chalk";
27
+ import fs3 from "fs";
28
+ import path3 from "path";
29
+ import os3 from "os";
30
+ import { isAddress as isAddress2 } from "viem";
31
+ import { privateKeyToAccount as privateKeyToAccount3 } from "viem/accounts";
32
+ import {
33
+ getEnvironmentConfig as getEnvironmentConfig2,
34
+ getAvailableEnvironments,
35
+ isEnvironmentAvailable,
36
+ getAllAppsByDeveloper as getAllAppsByDeveloper2,
37
+ getCategoryDescriptions,
38
+ fetchTemplateCatalog,
39
+ PRIMARY_LANGUAGES,
40
+ validateAppName,
41
+ validateImageReference,
42
+ validateFilePath,
43
+ validatePrivateKeyFormat,
44
+ extractAppNameFromImage,
45
+ UserApiClient as UserApiClient2
46
+ } from "@layr-labs/ecloud-sdk";
47
+
48
+ // src/utils/appResolver.ts
49
+ import { isAddress } from "viem";
50
+ import { privateKeyToAccount as privateKeyToAccount2 } from "viem/accounts";
51
+ import {
52
+ UserApiClient,
53
+ getAllAppsByDeveloper
54
+ } from "@layr-labs/ecloud-sdk";
55
+
56
+ // src/utils/viemClients.ts
57
+ import {
58
+ createPublicClient,
59
+ http
60
+ } from "viem";
61
+ import { privateKeyToAccount } from "viem/accounts";
62
+ import {
63
+ getEnvironmentConfig,
64
+ addHexPrefix,
65
+ createViemClients as sdkCreateViemClients,
66
+ getChainFromID
67
+ } from "@layr-labs/ecloud-sdk";
68
+ function createViemClients(options) {
69
+ const privateKey = addHexPrefix(options.privateKey);
70
+ const environmentConfig = getEnvironmentConfig(options.environment);
71
+ const rpcUrl = options.rpcUrl || environmentConfig.defaultRPCURL;
72
+ const chain = getChainFromID(environmentConfig.chainID);
73
+ const { publicClient, walletClient } = sdkCreateViemClients({
74
+ privateKey,
75
+ rpcUrl,
76
+ chainId: environmentConfig.chainID
77
+ });
78
+ const account = privateKeyToAccount(privateKey);
79
+ return {
80
+ publicClient,
81
+ walletClient,
82
+ chain,
83
+ address: account.address
84
+ };
85
+ }
86
+
87
+ // src/utils/globalConfig.ts
88
+ import * as fs from "fs";
89
+ import * as path from "path";
90
+ import * as os from "os";
91
+ import { load as loadYaml, dump as dumpYaml } from "js-yaml";
92
+ import { getBuildType } from "@layr-labs/ecloud-sdk";
93
+ import * as crypto from "crypto";
94
+ var GLOBAL_CONFIG_FILE = "config.yaml";
95
+ var PROFILE_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
96
+ function getGlobalConfigDir() {
97
+ const configHome = process.env.XDG_CONFIG_HOME;
98
+ let baseDir;
99
+ if (configHome && path.isAbsolute(configHome)) {
100
+ baseDir = configHome;
101
+ } else {
102
+ baseDir = path.join(os.homedir(), ".config");
103
+ }
104
+ const buildType = getBuildType();
105
+ const buildSuffix = buildType === "dev" ? "-dev" : "";
106
+ const configDirName = `ecloud${buildSuffix}`;
107
+ return path.join(baseDir, configDirName);
108
+ }
109
+ function getGlobalConfigPath() {
110
+ return path.join(getGlobalConfigDir(), GLOBAL_CONFIG_FILE);
111
+ }
112
+ function loadGlobalConfig() {
113
+ const configPath = getGlobalConfigPath();
114
+ if (!fs.existsSync(configPath)) {
115
+ return {
116
+ first_run: true
117
+ };
118
+ }
119
+ try {
120
+ const content = fs.readFileSync(configPath, "utf-8");
121
+ const config = loadYaml(content);
122
+ return config || { first_run: true };
123
+ } catch {
124
+ return {
125
+ first_run: true
126
+ };
127
+ }
128
+ }
129
+ function saveGlobalConfig(config) {
130
+ const configPath = getGlobalConfigPath();
131
+ const configDir = path.dirname(configPath);
132
+ fs.mkdirSync(configDir, { recursive: true, mode: 493 });
133
+ const content = dumpYaml(config, { lineWidth: -1 });
134
+ fs.writeFileSync(configPath, content, { mode: 420 });
135
+ }
136
+ function getDefaultEnvironment() {
137
+ const config = loadGlobalConfig();
138
+ return config.default_environment;
139
+ }
140
+ function getGlobalTelemetryPreference() {
141
+ const config = loadGlobalConfig();
142
+ return config.telemetry_enabled;
143
+ }
144
+ function getOrCreateUserUUID() {
145
+ const config = loadGlobalConfig();
146
+ if (config.user_uuid) {
147
+ return config.user_uuid;
148
+ }
149
+ const uuid = generateUUID();
150
+ config.user_uuid = uuid;
151
+ config.first_run = false;
152
+ saveGlobalConfig(config);
153
+ return uuid;
154
+ }
155
+ function generateUUID() {
156
+ const bytes = crypto.randomBytes(16);
157
+ bytes[6] = bytes[6] & 15 | 64;
158
+ bytes[8] = bytes[8] & 63 | 128;
159
+ const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0"));
160
+ return hex.slice(0, 4).join("") + hex.slice(4, 6).join("") + "-" + hex.slice(6, 8).join("") + "-" + hex.slice(8, 10).join("") + "-" + hex.slice(10, 12).join("") + "-" + hex.slice(12, 16).join("");
161
+ }
162
+
163
+ // src/utils/appNames.ts
164
+ import * as fs2 from "fs";
165
+ import * as path2 from "path";
166
+ import * as os2 from "os";
167
+ import { load as loadYaml2, dump as dumpYaml2 } from "js-yaml";
168
+ var CONFIG_DIR = path2.join(os2.homedir(), ".eigenx");
169
+ var APPS_DIR = path2.join(CONFIG_DIR, "apps");
170
+
171
+ // src/utils/prompts.ts
172
+ async function getPrivateKeyInteractive(privateKey) {
173
+ if (privateKey) {
174
+ if (!validatePrivateKeyFormat(privateKey)) {
175
+ throw new Error("Invalid private key format");
176
+ }
177
+ return privateKey;
178
+ }
179
+ const { getPrivateKeyWithSource: getPrivateKeyWithSource2 } = await import("@layr-labs/ecloud-sdk");
180
+ const result = await getPrivateKeyWithSource2({ privateKey: void 0 });
181
+ if (result) {
182
+ return result.key;
183
+ }
184
+ const key = await password({
185
+ message: "Enter private key:",
186
+ mask: true,
187
+ validate: (value) => {
188
+ if (!value.trim()) {
189
+ return "Private key is required";
190
+ }
191
+ if (!validatePrivateKeyFormat(value)) {
192
+ return "Invalid private key format (must be 64 hex characters, optionally prefixed with 0x)";
193
+ }
194
+ return true;
195
+ }
196
+ });
197
+ return key.trim();
198
+ }
199
+ var MAX_IMAGE_SIZE = 4 * 1024 * 1024;
200
+
201
+ // src/flags.ts
202
+ var commonFlags = {
203
+ environment: Flags.string({
204
+ required: false,
205
+ description: "Deployment environment to use",
206
+ env: "ECLOUD_ENV",
207
+ default: async () => getDefaultEnvironment() || (getBuildType2() === "dev" ? "sepolia-dev" : "sepolia")
208
+ }),
209
+ "private-key": Flags.string({
210
+ required: false,
211
+ description: "Private key for signing transactions",
212
+ env: "ECLOUD_PRIVATE_KEY"
213
+ }),
214
+ "rpc-url": Flags.string({
215
+ required: false,
216
+ description: "RPC URL to connect to blockchain",
217
+ env: "ECLOUD_RPC_URL"
218
+ }),
219
+ verbose: Flags.boolean({
220
+ required: false,
221
+ description: "Enable verbose logging (default: false)",
222
+ default: false
223
+ })
224
+ };
225
+
226
+ // src/client.ts
227
+ import { createWalletClient, custom } from "viem";
228
+ import { privateKeyToAccount as privateKeyToAccount4 } from "viem/accounts";
229
+ async function createBillingClient(flags) {
230
+ const result = await getPrivateKeyWithSource({
231
+ privateKey: flags["private-key"]
232
+ });
233
+ const privateKey = await getPrivateKeyInteractive(result?.key);
234
+ const account = privateKeyToAccount4(addHexPrefix2(privateKey));
235
+ const walletClient = createWalletClient({
236
+ account,
237
+ transport: custom({
238
+ async request() {
239
+ throw new Error("RPC not available - billing uses local signing only");
240
+ }
241
+ })
242
+ });
243
+ return createBillingModule({
244
+ verbose: flags.verbose ?? false,
245
+ walletClient,
246
+ skipTelemetry: true
247
+ // CLI already has telemetry, skip SDK telemetry
248
+ });
249
+ }
250
+
251
+ // src/commands/billing/top-up.ts
252
+ import {
253
+ getEnvironmentConfig as getEnvironmentConfig4,
254
+ USDCCreditsABI,
255
+ ERC20ABI
256
+ } from "@layr-labs/ecloud-sdk";
257
+ import { formatUnits } from "viem";
258
+ import chalk2 from "chalk";
259
+ import { input as input2 } from "@inquirer/prompts";
260
+
261
+ // src/telemetry.ts
262
+ import {
263
+ createTelemetryClient,
264
+ createAppEnvironment,
265
+ createMetricsContext,
266
+ addMetric,
267
+ addMetricWithDimensions,
268
+ emitMetrics,
269
+ getBuildType as getBuildType3
270
+ } from "@layr-labs/ecloud-sdk";
271
+ function createCLITelemetryClient() {
272
+ const userUUID = getOrCreateUserUUID();
273
+ const environment = createAppEnvironment(userUUID);
274
+ const telemetryEnabled = getGlobalTelemetryPreference();
275
+ return createTelemetryClient(environment, "ecloud-cli", {
276
+ telemetryEnabled: telemetryEnabled !== false
277
+ // Enabled by default, disabled only if explicitly set to false
278
+ });
279
+ }
280
+ async function withTelemetry(command, action) {
281
+ const client = createCLITelemetryClient();
282
+ const metrics = createMetricsContext();
283
+ metrics.properties["source"] = "ecloud-cli";
284
+ metrics.properties["command"] = command.id || command.constructor.name;
285
+ const environment = getDefaultEnvironment() || "sepolia";
286
+ metrics.properties["environment"] = environment;
287
+ const buildType = getBuildType3() || "prod";
288
+ metrics.properties["build_type"] = buildType;
289
+ const cliVersion = command.config.version;
290
+ if (cliVersion) {
291
+ metrics.properties["cli_version"] = cliVersion;
292
+ }
293
+ addMetric(metrics, "Count", 1);
294
+ let actionError;
295
+ let result;
296
+ try {
297
+ result = await action();
298
+ return result;
299
+ } catch (err) {
300
+ actionError = err instanceof Error ? err : new Error(String(err));
301
+ throw err;
302
+ } finally {
303
+ const resultValue = actionError ? "Failure" : "Success";
304
+ const dimensions = {};
305
+ if (actionError) {
306
+ dimensions["error"] = actionError.message;
307
+ }
308
+ addMetricWithDimensions(metrics, resultValue, 1, dimensions);
309
+ const duration = Date.now() - metrics.startTime.getTime();
310
+ addMetric(metrics, "DurationMilliseconds", duration);
311
+ try {
312
+ await emitMetrics(client, metrics);
313
+ await client.close();
314
+ } catch {
315
+ }
316
+ }
317
+ }
318
+
319
+ // src/commands/billing/top-up.ts
320
+ var POLL_INTERVAL_MS = 5e3;
321
+ var POLL_TIMEOUT_MS = 3 * 60 * 1e3;
322
+ var BillingTopUp = class _BillingTopUp extends Command {
323
+ static description = "Purchase EigenCompute credits with USDC";
324
+ static flags = {
325
+ ...commonFlags,
326
+ amount: Flags2.string({
327
+ required: false,
328
+ description: "Amount of USDC to spend (e.g., '50')"
329
+ }),
330
+ account: Flags2.string({
331
+ required: false,
332
+ description: "Target account address for purchaseCreditsFor (defaults to your wallet)"
333
+ }),
334
+ product: Flags2.string({
335
+ required: false,
336
+ description: "Product ID",
337
+ default: "compute",
338
+ options: ["compute"],
339
+ env: "ECLOUD_PRODUCT_ID"
340
+ })
341
+ };
342
+ async run() {
343
+ return withTelemetry(this, async () => {
344
+ const { flags } = await this.parse(_BillingTopUp);
345
+ const billing = await createBillingClient(flags);
346
+ const privateKey = await getPrivateKeyInteractive(flags["private-key"]);
347
+ const { publicClient, walletClient, address: walletAddress } = createViemClients({
348
+ privateKey,
349
+ rpcUrl: flags["rpc-url"],
350
+ environment: flags.environment
351
+ });
352
+ const environmentConfig = getEnvironmentConfig4(flags.environment);
353
+ const usdcCreditsAddress = environmentConfig.usdcCreditsAddress;
354
+ if (!usdcCreditsAddress) {
355
+ this.error(
356
+ `USDCCredits contract is not configured for environment "${flags.environment}". USDC credit purchasing is only available on environments with a deployed USDCCredits contract.`
357
+ );
358
+ }
359
+ const targetAccount = flags.account ?? walletAddress;
360
+ this.log(`
361
+ ${chalk2.bold("Purchase EigenCompute credits")}`);
362
+ this.log(`${chalk2.gray("\u2500".repeat(45))}`);
363
+ this.log(`
364
+ ${chalk2.bold("Wallet:")} ${walletAddress}`);
365
+ if (targetAccount !== walletAddress) {
366
+ this.log(` ${chalk2.bold("Target:")} ${targetAccount}`);
367
+ }
368
+ let currentCredits;
369
+ try {
370
+ const status = await billing.getStatus({
371
+ productId: flags.product
372
+ });
373
+ currentCredits = status.remainingCredits;
374
+ if (currentCredits !== void 0) {
375
+ this.log(` ${chalk2.bold("Credits:")} ${chalk2.cyan(`$${currentCredits.toFixed(2)}`)}`);
376
+ }
377
+ } catch {
378
+ this.debug("Could not fetch current credit balance");
379
+ }
380
+ const usdcAddress = await publicClient.readContract({
381
+ address: usdcCreditsAddress,
382
+ abi: USDCCreditsABI,
383
+ functionName: "usdc"
384
+ });
385
+ const minimumPurchase = await publicClient.readContract({
386
+ address: usdcCreditsAddress,
387
+ abi: USDCCreditsABI,
388
+ functionName: "minimumPurchase"
389
+ });
390
+ const usdcBalance = await publicClient.readContract({
391
+ address: usdcAddress,
392
+ abi: ERC20ABI,
393
+ functionName: "balanceOf",
394
+ args: [walletAddress]
395
+ });
396
+ const balanceFormatted = formatUnits(usdcBalance, 6);
397
+ this.log(` ${chalk2.bold("USDC:")} ${balanceFormatted} USDC`);
398
+ if (usdcBalance === BigInt(0)) {
399
+ this.log(`
400
+ ${chalk2.yellow(" No USDC in wallet.")}`);
401
+ this.log(` Send USDC on Sepolia to: ${chalk2.cyan(walletAddress)}`);
402
+ this.log(` Then re-run: ${chalk2.cyan("ecloud billing top-up")}
403
+ `);
404
+ return;
405
+ }
406
+ const minimumFormatted = formatUnits(minimumPurchase, 6);
407
+ const amountStr = flags.amount ?? await input2({
408
+ message: `How much USDC to spend on credits? (minimum: ${minimumFormatted})`,
409
+ validate: (val) => {
410
+ const n = parseFloat(val);
411
+ if (isNaN(n) || n <= 0) return "Enter a positive number";
412
+ const raw = BigInt(Math.round(n * 1e6));
413
+ if (raw < minimumPurchase)
414
+ return `Minimum purchase is ${minimumFormatted} USDC`;
415
+ if (raw > usdcBalance)
416
+ return `Insufficient balance. You have ${balanceFormatted} USDC`;
417
+ return true;
418
+ }
419
+ });
420
+ const amountFloat = parseFloat(amountStr);
421
+ const amountRaw = BigInt(Math.round(amountFloat * 1e6));
422
+ if (amountRaw < minimumPurchase) {
423
+ this.error(`Minimum purchase is ${minimumFormatted} USDC`);
424
+ }
425
+ if (amountRaw > usdcBalance) {
426
+ this.error(
427
+ `Insufficient USDC balance. You have ${balanceFormatted} USDC but requested ${amountFloat.toFixed(2)}`
428
+ );
429
+ }
430
+ this.log(`
431
+ Purchasing ${chalk2.bold(`$${amountFloat.toFixed(2)}`)} in credits...`);
432
+ const currentAllowance = await publicClient.readContract({
433
+ address: usdcAddress,
434
+ abi: ERC20ABI,
435
+ functionName: "allowance",
436
+ args: [walletAddress, usdcCreditsAddress]
437
+ });
438
+ if (currentAllowance < amountRaw) {
439
+ this.log(chalk2.gray(" Approving USDC spend..."));
440
+ const approveTx = await walletClient.writeContract({
441
+ address: usdcAddress,
442
+ abi: ERC20ABI,
443
+ functionName: "approve",
444
+ args: [usdcCreditsAddress, amountRaw],
445
+ chain: walletClient.chain,
446
+ account: walletClient.account
447
+ });
448
+ await publicClient.waitForTransactionReceipt({ hash: approveTx });
449
+ this.log(` ${chalk2.green("\u2713")} Approved`);
450
+ }
451
+ this.log(chalk2.gray(" Submitting credit purchase..."));
452
+ const purchaseTx = await walletClient.writeContract({
453
+ address: usdcCreditsAddress,
454
+ abi: USDCCreditsABI,
455
+ functionName: "purchaseCreditsFor",
456
+ args: [amountRaw, targetAccount],
457
+ chain: walletClient.chain,
458
+ account: walletClient.account
459
+ });
460
+ const receipt = await publicClient.waitForTransactionReceipt({ hash: purchaseTx });
461
+ this.log(` ${chalk2.green("\u2713")} Transaction confirmed: ${receipt.transactionHash}`);
462
+ this.log(chalk2.gray("\n Waiting for credits to appear..."));
463
+ const startTime = Date.now();
464
+ while (Date.now() - startTime < POLL_TIMEOUT_MS) {
465
+ await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
466
+ try {
467
+ const status = await billing.getStatus({
468
+ productId: flags.product
469
+ });
470
+ if (status.remainingCredits !== void 0 && (currentCredits === void 0 || status.remainingCredits > currentCredits)) {
471
+ this.log(
472
+ `
473
+ ${chalk2.green("\u2713")} Credits received! Balance: ${chalk2.cyan(`$${status.remainingCredits.toFixed(2)}`)}`
474
+ );
475
+ this.log();
476
+ return;
477
+ }
478
+ } catch {
479
+ this.debug("Error polling for credit balance");
480
+ }
481
+ }
482
+ this.log(
483
+ `
484
+ ${chalk2.yellow("\u26A0")} Credits haven't appeared yet. This can take a few minutes.`
485
+ );
486
+ this.log(` ${chalk2.gray("Check your balance:")} ecloud billing status
487
+ `);
488
+ });
489
+ }
490
+ };
491
+
492
+ // src/commands/billing/__tests__/top-up.test.ts
493
+ import { input as input3 } from "@inquirer/prompts";
494
+ vi.mock("../../../client", () => ({
495
+ createBillingClient: vi.fn()
496
+ }));
497
+ vi.mock("../../../utils/viemClients", () => ({
498
+ createViemClients: vi.fn()
499
+ }));
500
+ vi.mock("../../../telemetry", () => ({
501
+ withTelemetry: vi.fn((_cmd, fn) => fn())
502
+ }));
503
+ vi.mock("@inquirer/prompts", () => ({
504
+ input: vi.fn()
505
+ }));
506
+ vi.mock("@layr-labs/ecloud-sdk", async () => {
507
+ const actual = await vi.importActual("@layr-labs/ecloud-sdk");
508
+ return {
509
+ ...actual,
510
+ getEnvironmentConfig: vi.fn().mockReturnValue({
511
+ name: "sepolia",
512
+ build: "dev",
513
+ chainID: BigInt(11155111),
514
+ appControllerAddress: "0xa86DC1C47cb2518327fB4f9A1627F51966c83B92",
515
+ permissionControllerAddress: "0x44632dfBdCb6D3E21EF613B0ca8A6A0c618F5a37",
516
+ erc7702DelegatorAddress: "0x63c0c19a282a1b52b07dd5a65b58948a07dae32b",
517
+ kmsServerURL: "http://10.128.0.57:8080",
518
+ userApiServerURL: "https://userapi-compute-sepolia-dev.eigencloud.xyz",
519
+ defaultRPCURL: "https://ethereum-sepolia-rpc.publicnode.com",
520
+ usdcCreditsAddress: "0xbdA3897c3A428763B59015C64AB766c288C97376"
521
+ })
522
+ };
523
+ });
524
+ var WALLET_ADDRESS = "0x1234567890abcdef1234567890abcdef12345678";
525
+ var USDC_ADDRESS = "0xUSDCAddress0000000000000000000000000000";
526
+ var USDC_CREDITS_ADDRESS = "0xbdA3897c3A428763B59015C64AB766c288C97376";
527
+ var TX_HASH = "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890";
528
+ describe("ecloud billing top-up", () => {
529
+ let logOutput;
530
+ let mockBilling;
531
+ let mockPublicClient;
532
+ let mockWalletClient;
533
+ beforeEach(() => {
534
+ vi.useFakeTimers();
535
+ vi.clearAllMocks();
536
+ logOutput = [];
537
+ mockBilling = {
538
+ address: WALLET_ADDRESS,
539
+ getStatus: vi.fn()
540
+ };
541
+ createBillingClient.mockResolvedValue(mockBilling);
542
+ mockPublicClient = {
543
+ readContract: vi.fn(),
544
+ waitForTransactionReceipt: vi.fn().mockResolvedValue({
545
+ transactionHash: TX_HASH
546
+ })
547
+ };
548
+ mockWalletClient = {
549
+ writeContract: vi.fn().mockResolvedValue(TX_HASH),
550
+ chain: { id: 11155111 },
551
+ account: { address: WALLET_ADDRESS }
552
+ };
553
+ createViemClients.mockReturnValue({
554
+ publicClient: mockPublicClient,
555
+ walletClient: mockWalletClient,
556
+ address: WALLET_ADDRESS,
557
+ chain: { id: 11155111 }
558
+ });
559
+ input3.mockResolvedValue("50");
560
+ });
561
+ afterEach(() => {
562
+ vi.useRealTimers();
563
+ });
564
+ function setupContractReads(overrides = {}) {
565
+ const {
566
+ usdcAddress = USDC_ADDRESS,
567
+ minimumPurchase = BigInt(1e6),
568
+ // 1 USDC
569
+ balanceOf = BigInt(1e8),
570
+ // 100 USDC
571
+ allowance = BigInt(0)
572
+ } = overrides;
573
+ mockPublicClient.readContract.mockImplementation(({ functionName }) => {
574
+ switch (functionName) {
575
+ case "usdc":
576
+ return Promise.resolve(usdcAddress);
577
+ case "minimumPurchase":
578
+ return Promise.resolve(minimumPurchase);
579
+ case "balanceOf":
580
+ return Promise.resolve(balanceOf);
581
+ case "allowance":
582
+ return Promise.resolve(allowance);
583
+ default:
584
+ return Promise.reject(new Error(`Unexpected readContract: ${functionName}`));
585
+ }
586
+ });
587
+ }
588
+ function createCommand(flags = {}) {
589
+ const cmd = new BillingTopUp([], {});
590
+ cmd.parse = vi.fn().mockResolvedValue({
591
+ flags: {
592
+ product: "compute",
593
+ "private-key": "0xdeadbeef",
594
+ environment: "sepolia-dev",
595
+ ...flags
596
+ }
597
+ });
598
+ cmd.log = vi.fn((...args) => logOutput.push(args.join(" ")));
599
+ cmd.debug = vi.fn();
600
+ cmd.error = vi.fn((msg) => {
601
+ throw new Error(msg);
602
+ });
603
+ return cmd;
604
+ }
605
+ it("happy path: sufficient balance, approval needed, purchase succeeds", async () => {
606
+ setupContractReads();
607
+ mockBilling.getStatus.mockResolvedValueOnce({ subscriptionStatus: "active", remainingCredits: 10 }).mockResolvedValueOnce({ subscriptionStatus: "active", remainingCredits: 60 });
608
+ const cmd = createCommand({ amount: "50" });
609
+ const promise = cmd.run();
610
+ for (let i = 0; i < 10; i++) {
611
+ await vi.advanceTimersByTimeAsync(5e3);
612
+ }
613
+ await promise;
614
+ const fullOutput = logOutput.join("\n");
615
+ expect(fullOutput).toContain(WALLET_ADDRESS);
616
+ expect(fullOutput).toContain("$10.00");
617
+ expect(fullOutput).toContain("100 USDC");
618
+ expect(fullOutput).toContain("Approving USDC spend");
619
+ expect(fullOutput).toContain("Approved");
620
+ expect(fullOutput).toContain("Submitting credit purchase");
621
+ expect(fullOutput).toContain("Transaction confirmed");
622
+ expect(fullOutput).toContain("Credits received");
623
+ expect(fullOutput).toContain("$60.00");
624
+ expect(mockWalletClient.writeContract).toHaveBeenCalledWith(
625
+ expect.objectContaining({
626
+ functionName: "approve",
627
+ args: [USDC_CREDITS_ADDRESS, BigInt(5e7)]
628
+ })
629
+ );
630
+ expect(mockWalletClient.writeContract).toHaveBeenCalledWith(
631
+ expect.objectContaining({
632
+ functionName: "purchaseCreditsFor",
633
+ args: [BigInt(5e7), WALLET_ADDRESS]
634
+ })
635
+ );
636
+ });
637
+ it("zero USDC balance: exits with fund wallet message", async () => {
638
+ setupContractReads({ balanceOf: BigInt(0) });
639
+ mockBilling.getStatus.mockResolvedValue({ subscriptionStatus: "inactive" });
640
+ const cmd = createCommand({ amount: "50" });
641
+ await cmd.run();
642
+ const fullOutput = logOutput.join("\n");
643
+ expect(fullOutput).toContain("No USDC in wallet");
644
+ expect(fullOutput).toContain("Send USDC on Sepolia to");
645
+ expect(fullOutput).toContain(WALLET_ADDRESS);
646
+ expect(mockWalletClient.writeContract).not.toHaveBeenCalled();
647
+ });
648
+ it("below minimum purchase: shows error", async () => {
649
+ setupContractReads({ minimumPurchase: BigInt(1e7) });
650
+ mockBilling.getStatus.mockResolvedValue({ subscriptionStatus: "inactive" });
651
+ const cmd = createCommand({ amount: "5" });
652
+ await expect(cmd.run()).rejects.toThrow("Minimum purchase is 10 USDC");
653
+ });
654
+ it("--account flag: passes different address to purchaseCreditsFor", async () => {
655
+ const targetAccount = "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
656
+ setupContractReads();
657
+ mockBilling.getStatus.mockResolvedValueOnce({ subscriptionStatus: "active", remainingCredits: 10 }).mockResolvedValueOnce({ subscriptionStatus: "active", remainingCredits: 60 });
658
+ const cmd = createCommand({ amount: "50", account: targetAccount });
659
+ const promise = cmd.run();
660
+ for (let i = 0; i < 10; i++) {
661
+ await vi.advanceTimersByTimeAsync(5e3);
662
+ }
663
+ await promise;
664
+ const fullOutput = logOutput.join("\n");
665
+ expect(fullOutput).toContain(targetAccount);
666
+ expect(mockWalletClient.writeContract).toHaveBeenCalledWith(
667
+ expect.objectContaining({
668
+ functionName: "purchaseCreditsFor",
669
+ args: [BigInt(5e7), targetAccount]
670
+ })
671
+ );
672
+ });
673
+ it("allowance already sufficient: skips approve step", async () => {
674
+ setupContractReads({ allowance: BigInt(1e8) });
675
+ mockBilling.getStatus.mockResolvedValueOnce({ subscriptionStatus: "active", remainingCredits: 10 }).mockResolvedValueOnce({ subscriptionStatus: "active", remainingCredits: 60 });
676
+ const cmd = createCommand({ amount: "50" });
677
+ const promise = cmd.run();
678
+ for (let i = 0; i < 10; i++) {
679
+ await vi.advanceTimersByTimeAsync(5e3);
680
+ }
681
+ await promise;
682
+ const fullOutput = logOutput.join("\n");
683
+ expect(fullOutput).not.toContain("Approving USDC spend");
684
+ expect(mockWalletClient.writeContract).toHaveBeenCalledTimes(1);
685
+ expect(mockWalletClient.writeContract).toHaveBeenCalledWith(
686
+ expect.objectContaining({
687
+ functionName: "purchaseCreditsFor"
688
+ })
689
+ );
690
+ });
691
+ it("billing API poll timeout: shows timeout message", async () => {
692
+ setupContractReads();
693
+ mockBilling.getStatus.mockResolvedValue({
694
+ subscriptionStatus: "active",
695
+ remainingCredits: 10
696
+ });
697
+ const cmd = createCommand({ amount: "50" });
698
+ const promise = cmd.run();
699
+ await vi.advanceTimersByTimeAsync(2e5);
700
+ await promise;
701
+ const fullOutput = logOutput.join("\n");
702
+ expect(fullOutput).toContain("Credits haven't appeared yet");
703
+ expect(fullOutput).toContain("ecloud billing status");
704
+ });
705
+ it("uses --amount flag when provided (skips prompt)", async () => {
706
+ setupContractReads();
707
+ mockBilling.getStatus.mockResolvedValueOnce({ subscriptionStatus: "inactive" }).mockResolvedValueOnce({ subscriptionStatus: "active", remainingCredits: 100 });
708
+ const cmd = createCommand({ amount: "100" });
709
+ const promise = cmd.run();
710
+ for (let i = 0; i < 10; i++) {
711
+ await vi.advanceTimersByTimeAsync(5e3);
712
+ }
713
+ await promise;
714
+ expect(input3).not.toHaveBeenCalled();
715
+ });
716
+ it("does not fail if status check errors", async () => {
717
+ setupContractReads();
718
+ mockBilling.getStatus.mockRejectedValue(new Error("API unavailable"));
719
+ const cmd = createCommand({ amount: "50" });
720
+ const promise = cmd.run();
721
+ await vi.advanceTimersByTimeAsync(2e5);
722
+ await promise;
723
+ const fullOutput = logOutput.join("\n");
724
+ expect(fullOutput).toContain("Submitting credit purchase");
725
+ expect(fullOutput).toContain("Transaction confirmed");
726
+ expect(fullOutput).toContain("Credits haven't appeared yet");
727
+ });
728
+ });
729
+ //# sourceMappingURL=top-up.test.js.map