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

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 (70) hide show
  1. package/VERSION +2 -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 +1 -0
  8. package/dist/commands/auth/whoami.js.map +1 -1
  9. package/dist/commands/billing/__tests__/status.test.js +89 -22
  10. package/dist/commands/billing/__tests__/status.test.js.map +1 -1
  11. package/dist/commands/billing/__tests__/subscribe.test.js +88 -20
  12. package/dist/commands/billing/__tests__/subscribe.test.js.map +1 -1
  13. package/dist/commands/billing/__tests__/top-up.test.js +139 -201
  14. package/dist/commands/billing/__tests__/top-up.test.js.map +1 -1
  15. package/dist/commands/billing/cancel.js +88 -19
  16. package/dist/commands/billing/cancel.js.map +1 -1
  17. package/dist/commands/billing/status.js +89 -21
  18. package/dist/commands/billing/status.js.map +1 -1
  19. package/dist/commands/billing/subscribe.js +88 -19
  20. package/dist/commands/billing/subscribe.js.map +1 -1
  21. package/dist/commands/billing/top-up.js +102 -91
  22. package/dist/commands/billing/top-up.js.map +1 -1
  23. package/dist/commands/compute/app/configure/tls.js +0 -0
  24. package/dist/commands/compute/app/create.js +1 -0
  25. package/dist/commands/compute/app/create.js.map +1 -1
  26. package/dist/commands/compute/app/deploy.js +38 -24
  27. package/dist/commands/compute/app/deploy.js.map +1 -1
  28. package/dist/commands/compute/app/info.js +2 -1
  29. package/dist/commands/compute/app/info.js.map +1 -1
  30. package/dist/commands/compute/app/list.js +2 -1
  31. package/dist/commands/compute/app/list.js.map +1 -1
  32. package/dist/commands/compute/app/logs.js +5 -8
  33. package/dist/commands/compute/app/logs.js.map +1 -1
  34. package/dist/commands/compute/app/profile/set.js +5 -8
  35. package/dist/commands/compute/app/profile/set.js.map +1 -1
  36. package/dist/commands/compute/app/releases.js +2 -1
  37. package/dist/commands/compute/app/releases.js.map +1 -1
  38. package/dist/commands/compute/app/start.js +5 -8
  39. package/dist/commands/compute/app/start.js.map +1 -1
  40. package/dist/commands/compute/app/stop.js +5 -8
  41. package/dist/commands/compute/app/stop.js.map +1 -1
  42. package/dist/commands/compute/app/terminate.js +5 -8
  43. package/dist/commands/compute/app/terminate.js.map +1 -1
  44. package/dist/commands/compute/app/upgrade.js +36 -14
  45. package/dist/commands/compute/app/upgrade.js.map +1 -1
  46. package/dist/commands/compute/build/info.js +9 -12
  47. package/dist/commands/compute/build/info.js.map +1 -1
  48. package/dist/commands/compute/build/list.js +9 -12
  49. package/dist/commands/compute/build/list.js.map +1 -1
  50. package/dist/commands/compute/build/logs.js +9 -12
  51. package/dist/commands/compute/build/logs.js.map +1 -1
  52. package/dist/commands/compute/build/status.js +9 -12
  53. package/dist/commands/compute/build/status.js.map +1 -1
  54. package/dist/commands/compute/build/submit.js +32 -10
  55. package/dist/commands/compute/build/submit.js.map +1 -1
  56. package/dist/commands/compute/build/verify.js +9 -12
  57. package/dist/commands/compute/build/verify.js.map +1 -1
  58. package/dist/commands/compute/environment/list.js +0 -0
  59. package/dist/commands/compute/environment/set.js +1 -0
  60. package/dist/commands/compute/environment/set.js.map +1 -1
  61. package/dist/commands/compute/environment/show.js +0 -0
  62. package/dist/commands/compute/undelegate.js +5 -8
  63. package/dist/commands/compute/undelegate.js.map +1 -1
  64. package/dist/commands/telemetry/disable.js +0 -0
  65. package/dist/commands/telemetry/enable.js +0 -0
  66. package/dist/commands/telemetry/status.js +0 -0
  67. package/dist/commands/upgrade.js +0 -0
  68. package/dist/commands/version.js +0 -0
  69. package/package.json +15 -14
  70. package/LICENSE +0 -7
@@ -12,9 +12,7 @@ import {
12
12
  createBillingModule,
13
13
  createBuildModule,
14
14
  getEnvironmentConfig as getEnvironmentConfig3,
15
- requirePrivateKey,
16
- getPrivateKeyWithSource,
17
- addHexPrefix as addHexPrefix2
15
+ requirePrivateKey
18
16
  } from "@layr-labs/ecloud-sdk";
19
17
 
20
18
  // src/flags.ts
@@ -169,6 +167,7 @@ var CONFIG_DIR = path2.join(os2.homedir(), ".eigenx");
169
167
  var APPS_DIR = path2.join(CONFIG_DIR, "apps");
170
168
 
171
169
  // src/utils/prompts.ts
170
+ import { execSync } from "child_process";
172
171
  async function getPrivateKeyInteractive(privateKey) {
173
172
  if (privateKey) {
174
173
  if (!validatePrivateKeyFormat(privateKey)) {
@@ -176,8 +175,8 @@ async function getPrivateKeyInteractive(privateKey) {
176
175
  }
177
176
  return privateKey;
178
177
  }
179
- const { getPrivateKeyWithSource: getPrivateKeyWithSource2 } = await import("@layr-labs/ecloud-sdk");
180
- const result = await getPrivateKeyWithSource2({ privateKey: void 0 });
178
+ const { getPrivateKeyWithSource } = await import("@layr-labs/ecloud-sdk");
179
+ const result = await getPrivateKeyWithSource({ privateKey: void 0 });
181
180
  if (result) {
182
181
  return result.key;
183
182
  }
@@ -196,6 +195,50 @@ async function getPrivateKeyInteractive(privateKey) {
196
195
  });
197
196
  return key.trim();
198
197
  }
198
+ async function getEnvironmentInteractive(environment) {
199
+ if (environment) {
200
+ try {
201
+ getEnvironmentConfig2(environment);
202
+ if (!isEnvironmentAvailable(environment)) {
203
+ throw new Error(`Environment ${environment} is not available in this build`);
204
+ }
205
+ return environment;
206
+ } catch {
207
+ }
208
+ }
209
+ const availableEnvs = getAvailableEnvironments();
210
+ let defaultEnv;
211
+ const configDefaultEnv = getDefaultEnvironment();
212
+ if (configDefaultEnv && availableEnvs.includes(configDefaultEnv)) {
213
+ try {
214
+ getEnvironmentConfig2(configDefaultEnv);
215
+ defaultEnv = configDefaultEnv;
216
+ } catch {
217
+ }
218
+ }
219
+ const choices = [];
220
+ if (availableEnvs.includes("sepolia")) {
221
+ choices.push({ name: "sepolia - Ethereum Sepolia testnet", value: "sepolia" });
222
+ }
223
+ if (availableEnvs.includes("sepolia-dev")) {
224
+ choices.push({ name: "sepolia-dev - Ethereum Sepolia testnet (dev)", value: "sepolia-dev" });
225
+ }
226
+ if (availableEnvs.includes("mainnet-alpha")) {
227
+ choices.push({
228
+ name: "mainnet-alpha - Ethereum mainnet (\u26A0\uFE0F uses real funds)",
229
+ value: "mainnet-alpha"
230
+ });
231
+ }
232
+ if (choices.length === 0) {
233
+ throw new Error("No environments available in this build");
234
+ }
235
+ const env = await select({
236
+ message: "Select environment:",
237
+ choices,
238
+ default: defaultEnv
239
+ });
240
+ return env;
241
+ }
199
242
  var MAX_IMAGE_SIZE = 4 * 1024 * 1024;
200
243
 
201
244
  // src/flags.ts
@@ -222,38 +265,41 @@ var commonFlags = {
222
265
  default: false
223
266
  })
224
267
  };
268
+ async function validateCommonFlags(flags, options) {
269
+ flags["environment"] = await getEnvironmentInteractive(flags["environment"]);
270
+ if (options?.requirePrivateKey !== false) {
271
+ flags["private-key"] = await getPrivateKeyInteractive(flags["private-key"]);
272
+ }
273
+ return flags;
274
+ }
225
275
 
226
276
  // src/client.ts
227
- import { createWalletClient, custom } from "viem";
228
- import { privateKeyToAccount as privateKeyToAccount4 } from "viem/accounts";
229
277
  async function createBillingClient(flags) {
230
- const result = await getPrivateKeyWithSource({
278
+ flags = await validateCommonFlags(flags);
279
+ const environment = flags.environment;
280
+ const environmentConfig = getEnvironmentConfig3(environment);
281
+ const rpcUrl = flags["rpc-url"] || environmentConfig.billingRPCURL || environmentConfig.defaultRPCURL;
282
+ const { key: privateKey, source } = await requirePrivateKey({
231
283
  privateKey: flags["private-key"]
232
284
  });
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
- })
285
+ if (flags.verbose) {
286
+ console.log(`Using private key from: ${source}`);
287
+ }
288
+ const { walletClient, publicClient } = createViemClients({
289
+ privateKey,
290
+ rpcUrl,
291
+ environment
242
292
  });
243
293
  return createBillingModule({
244
- verbose: flags.verbose ?? false,
294
+ verbose: flags.verbose,
245
295
  walletClient,
296
+ publicClient,
297
+ environment,
246
298
  skipTelemetry: true
247
- // CLI already has telemetry, skip SDK telemetry
248
299
  });
249
300
  }
250
301
 
251
302
  // src/commands/billing/top-up.ts
252
- import {
253
- getEnvironmentConfig as getEnvironmentConfig4,
254
- USDCCreditsABI,
255
- ERC20ABI
256
- } from "@layr-labs/ecloud-sdk";
257
303
  import { formatUnits } from "viem";
258
304
  import chalk2 from "chalk";
259
305
  import { input as input2 } from "@inquirer/prompts";
@@ -343,19 +389,7 @@ var BillingTopUp = class _BillingTopUp extends Command {
343
389
  return withTelemetry(this, async () => {
344
390
  const { flags } = await this.parse(_BillingTopUp);
345
391
  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
- }
392
+ const walletAddress = billing.address;
359
393
  const targetAccount = flags.account ?? walletAddress;
360
394
  this.log(`
361
395
  ${chalk2.bold("Purchase EigenCompute credits")}`);
@@ -365,34 +399,22 @@ ${chalk2.bold("Purchase EigenCompute credits")}`);
365
399
  if (targetAccount !== walletAddress) {
366
400
  this.log(` ${chalk2.bold("Target:")} ${targetAccount}`);
367
401
  }
368
- let currentCredits;
402
+ let baselineTotal;
369
403
  try {
370
404
  const status = await billing.getStatus({
371
405
  productId: flags.product
372
406
  });
373
- currentCredits = status.remainingCredits;
374
- if (currentCredits !== void 0) {
375
- this.log(` ${chalk2.bold("Credits:")} ${chalk2.cyan(`$${currentCredits.toFixed(2)}`)}`);
407
+ const remaining = status.remainingCredits ?? 0;
408
+ const applied = status.creditsApplied ?? 0;
409
+ baselineTotal = remaining + applied;
410
+ if (status.remainingCredits !== void 0) {
411
+ this.log(` ${chalk2.bold("Credits:")} ${chalk2.cyan(`$${status.remainingCredits.toFixed(2)}`)}`);
376
412
  }
377
413
  } catch {
378
414
  this.debug("Could not fetch current credit balance");
379
415
  }
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
- });
416
+ const onChainState = await billing.getTopUpInfo();
417
+ const { usdcBalance, minimumPurchase } = onChainState;
396
418
  const balanceFormatted = formatUnits(usdcBalance, 6);
397
419
  this.log(` ${chalk2.bold("USDC:")} ${balanceFormatted} USDC`);
398
420
  if (usdcBalance === BigInt(0)) {
@@ -429,36 +451,11 @@ ${chalk2.yellow(" No USDC in wallet.")}`);
429
451
  }
430
452
  this.log(`
431
453
  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
454
+ const { txHash } = await billing.topUp({
455
+ amount: amountRaw,
456
+ account: targetAccount
459
457
  });
460
- const receipt = await publicClient.waitForTransactionReceipt({ hash: purchaseTx });
461
- this.log(` ${chalk2.green("\u2713")} Transaction confirmed: ${receipt.transactionHash}`);
458
+ this.log(` ${chalk2.green("\u2713")} Transaction confirmed: ${txHash}`);
462
459
  this.log(chalk2.gray("\n Waiting for credits to appear..."));
463
460
  const startTime = Date.now();
464
461
  while (Date.now() - startTime < POLL_TIMEOUT_MS) {
@@ -467,11 +464,25 @@ ${chalk2.yellow(" No USDC in wallet.")}`);
467
464
  const status = await billing.getStatus({
468
465
  productId: flags.product
469
466
  });
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
- );
467
+ const remaining = status.remainingCredits ?? 0;
468
+ const applied = status.creditsApplied ?? 0;
469
+ const currentTotal = remaining + applied;
470
+ this.debug(`Poll: remaining=${remaining}, applied=${applied}, total=${currentTotal}, baseline=${baselineTotal}`);
471
+ if (baselineTotal === void 0 || currentTotal > baselineTotal) {
472
+ const creditsAdded = baselineTotal !== void 0 ? currentTotal - baselineTotal : void 0;
473
+ const isMatched = creditsAdded !== void 0 && Math.abs(creditsAdded - amountFloat * 2) < 0.01;
474
+ const appliedFromTopUp = creditsAdded !== void 0 ? creditsAdded - remaining : 0;
475
+ this.log(`
476
+ ${chalk2.green("\u2713")} Credits received: ${chalk2.cyan(`$${(creditsAdded ?? amountFloat).toFixed(2)}`)}`);
477
+ if (isMatched) {
478
+ this.log(` ${chalk2.green("\u2713")} Includes $${amountFloat.toFixed(2)} match bonus!`);
479
+ }
480
+ if (remaining > 0) {
481
+ this.log(` Remaining balance: ${chalk2.cyan(`$${remaining.toFixed(2)}`)}`);
482
+ }
483
+ if (appliedFromTopUp > 0) {
484
+ this.log(` ${chalk2.gray(`$${appliedFromTopUp.toFixed(2)} applied to current bill`)}`);
485
+ }
475
486
  this.log();
476
487
  return;
477
488
  }
@@ -494,95 +505,47 @@ import { input as input3 } from "@inquirer/prompts";
494
505
  vi.mock("../../../client", () => ({
495
506
  createBillingClient: vi.fn()
496
507
  }));
497
- vi.mock("../../../utils/viemClients", () => ({
498
- createViemClients: vi.fn()
499
- }));
500
508
  vi.mock("../../../telemetry", () => ({
501
509
  withTelemetry: vi.fn((_cmd, fn) => fn())
502
510
  }));
503
511
  vi.mock("@inquirer/prompts", () => ({
504
512
  input: vi.fn()
505
513
  }));
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
514
  var WALLET_ADDRESS = "0x1234567890abcdef1234567890abcdef12345678";
525
- var USDC_ADDRESS = "0xUSDCAddress0000000000000000000000000000";
526
- var USDC_CREDITS_ADDRESS = "0xbdA3897c3A428763B59015C64AB766c288C97376";
527
515
  var TX_HASH = "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890";
528
516
  describe("ecloud billing top-up", () => {
529
517
  let logOutput;
530
518
  let mockBilling;
531
- let mockPublicClient;
532
- let mockWalletClient;
533
519
  beforeEach(() => {
534
520
  vi.useFakeTimers();
535
521
  vi.clearAllMocks();
536
522
  logOutput = [];
537
523
  mockBilling = {
538
524
  address: WALLET_ADDRESS,
539
- getStatus: vi.fn()
525
+ getStatus: vi.fn(),
526
+ getTopUpInfo: vi.fn(),
527
+ topUp: vi.fn()
540
528
  };
541
529
  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
530
  input3.mockResolvedValue("50");
560
531
  });
561
532
  afterEach(() => {
562
533
  vi.useRealTimers();
563
534
  });
564
- function setupContractReads(overrides = {}) {
535
+ function setupOnChainState(overrides = {}) {
565
536
  const {
566
- usdcAddress = USDC_ADDRESS,
537
+ usdcAddress = "0xUSDCAddress0000000000000000000000000000",
567
538
  minimumPurchase = BigInt(1e6),
568
539
  // 1 USDC
569
- balanceOf = BigInt(1e8),
540
+ usdcBalance = BigInt(1e8),
570
541
  // 100 USDC
571
- allowance = BigInt(0)
542
+ currentAllowance = BigInt(0)
572
543
  } = 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
- }
544
+ mockBilling.getTopUpInfo.mockResolvedValue({
545
+ usdcAddress,
546
+ minimumPurchase,
547
+ usdcBalance,
548
+ currentAllowance
586
549
  });
587
550
  }
588
551
  function createCommand(flags = {}) {
@@ -602,8 +565,9 @@ describe("ecloud billing top-up", () => {
602
565
  });
603
566
  return cmd;
604
567
  }
605
- it("happy path: sufficient balance, approval needed, purchase succeeds", async () => {
606
- setupContractReads();
568
+ it("happy path: sufficient balance, purchase succeeds", async () => {
569
+ setupOnChainState();
570
+ mockBilling.topUp.mockResolvedValue({ txHash: TX_HASH, walletAddress: WALLET_ADDRESS });
607
571
  mockBilling.getStatus.mockResolvedValueOnce({ subscriptionStatus: "active", remainingCredits: 10 }).mockResolvedValueOnce({ subscriptionStatus: "active", remainingCredits: 60 });
608
572
  const cmd = createCommand({ amount: "50" });
609
573
  const promise = cmd.run();
@@ -615,27 +579,17 @@ describe("ecloud billing top-up", () => {
615
579
  expect(fullOutput).toContain(WALLET_ADDRESS);
616
580
  expect(fullOutput).toContain("$10.00");
617
581
  expect(fullOutput).toContain("100 USDC");
618
- expect(fullOutput).toContain("Approving USDC spend");
619
- expect(fullOutput).toContain("Approved");
620
- expect(fullOutput).toContain("Submitting credit purchase");
582
+ expect(fullOutput).toContain("Purchasing");
621
583
  expect(fullOutput).toContain("Transaction confirmed");
622
584
  expect(fullOutput).toContain("Credits received");
623
585
  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
- );
586
+ expect(mockBilling.topUp).toHaveBeenCalledWith({
587
+ amount: BigInt(5e7),
588
+ account: WALLET_ADDRESS
589
+ });
636
590
  });
637
591
  it("zero USDC balance: exits with fund wallet message", async () => {
638
- setupContractReads({ balanceOf: BigInt(0) });
592
+ setupOnChainState({ usdcBalance: BigInt(0) });
639
593
  mockBilling.getStatus.mockResolvedValue({ subscriptionStatus: "inactive" });
640
594
  const cmd = createCommand({ amount: "50" });
641
595
  await cmd.run();
@@ -643,17 +597,18 @@ describe("ecloud billing top-up", () => {
643
597
  expect(fullOutput).toContain("No USDC in wallet");
644
598
  expect(fullOutput).toContain("Send USDC on Sepolia to");
645
599
  expect(fullOutput).toContain(WALLET_ADDRESS);
646
- expect(mockWalletClient.writeContract).not.toHaveBeenCalled();
600
+ expect(mockBilling.topUp).not.toHaveBeenCalled();
647
601
  });
648
602
  it("below minimum purchase: shows error", async () => {
649
- setupContractReads({ minimumPurchase: BigInt(1e7) });
603
+ setupOnChainState({ minimumPurchase: BigInt(1e7) });
650
604
  mockBilling.getStatus.mockResolvedValue({ subscriptionStatus: "inactive" });
651
605
  const cmd = createCommand({ amount: "5" });
652
606
  await expect(cmd.run()).rejects.toThrow("Minimum purchase is 10 USDC");
653
607
  });
654
- it("--account flag: passes different address to purchaseCreditsFor", async () => {
608
+ it("--account flag: passes different address to topUp", async () => {
655
609
  const targetAccount = "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
656
- setupContractReads();
610
+ setupOnChainState();
611
+ mockBilling.topUp.mockResolvedValue({ txHash: TX_HASH, walletAddress: WALLET_ADDRESS });
657
612
  mockBilling.getStatus.mockResolvedValueOnce({ subscriptionStatus: "active", remainingCredits: 10 }).mockResolvedValueOnce({ subscriptionStatus: "active", remainingCredits: 60 });
658
613
  const cmd = createCommand({ amount: "50", account: targetAccount });
659
614
  const promise = cmd.run();
@@ -663,33 +618,14 @@ describe("ecloud billing top-up", () => {
663
618
  await promise;
664
619
  const fullOutput = logOutput.join("\n");
665
620
  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
- );
621
+ expect(mockBilling.topUp).toHaveBeenCalledWith({
622
+ amount: BigInt(5e7),
623
+ account: targetAccount
624
+ });
690
625
  });
691
626
  it("billing API poll timeout: shows timeout message", async () => {
692
- setupContractReads();
627
+ setupOnChainState();
628
+ mockBilling.topUp.mockResolvedValue({ txHash: TX_HASH, walletAddress: WALLET_ADDRESS });
693
629
  mockBilling.getStatus.mockResolvedValue({
694
630
  subscriptionStatus: "active",
695
631
  remainingCredits: 10
@@ -703,7 +639,8 @@ describe("ecloud billing top-up", () => {
703
639
  expect(fullOutput).toContain("ecloud billing status");
704
640
  });
705
641
  it("uses --amount flag when provided (skips prompt)", async () => {
706
- setupContractReads();
642
+ setupOnChainState();
643
+ mockBilling.topUp.mockResolvedValue({ txHash: TX_HASH, walletAddress: WALLET_ADDRESS });
707
644
  mockBilling.getStatus.mockResolvedValueOnce({ subscriptionStatus: "inactive" }).mockResolvedValueOnce({ subscriptionStatus: "active", remainingCredits: 100 });
708
645
  const cmd = createCommand({ amount: "100" });
709
646
  const promise = cmd.run();
@@ -714,14 +651,15 @@ describe("ecloud billing top-up", () => {
714
651
  expect(input3).not.toHaveBeenCalled();
715
652
  });
716
653
  it("does not fail if status check errors", async () => {
717
- setupContractReads();
654
+ setupOnChainState();
655
+ mockBilling.topUp.mockResolvedValue({ txHash: TX_HASH, walletAddress: WALLET_ADDRESS });
718
656
  mockBilling.getStatus.mockRejectedValue(new Error("API unavailable"));
719
657
  const cmd = createCommand({ amount: "50" });
720
658
  const promise = cmd.run();
721
659
  await vi.advanceTimersByTimeAsync(2e5);
722
660
  await promise;
723
661
  const fullOutput = logOutput.join("\n");
724
- expect(fullOutput).toContain("Submitting credit purchase");
662
+ expect(fullOutput).toContain("Purchasing");
725
663
  expect(fullOutput).toContain("Transaction confirmed");
726
664
  expect(fullOutput).toContain("Credits haven't appeared yet");
727
665
  });