@layr-labs/ecloud-cli 1.0.0-dev.6 → 1.0.0-dev.8

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 (82) hide show
  1. package/VERSION +2 -2
  2. package/dist/commands/admin/admins/add.js +7 -1
  3. package/dist/commands/admin/admins/add.js.map +1 -1
  4. package/dist/commands/admin/admins/list.js +7 -1
  5. package/dist/commands/admin/admins/list.js.map +1 -1
  6. package/dist/commands/admin/admins/remove.js +7 -1
  7. package/dist/commands/admin/admins/remove.js.map +1 -1
  8. package/dist/commands/admin/coupons/create.js +7 -1
  9. package/dist/commands/admin/coupons/create.js.map +1 -1
  10. package/dist/commands/admin/coupons/deactivate.js +7 -1
  11. package/dist/commands/admin/coupons/deactivate.js.map +1 -1
  12. package/dist/commands/admin/coupons/get.js +7 -1
  13. package/dist/commands/admin/coupons/get.js.map +1 -1
  14. package/dist/commands/admin/coupons/list.js +7 -1
  15. package/dist/commands/admin/coupons/list.js.map +1 -1
  16. package/dist/commands/admin/coupons/redeem.js +7 -1
  17. package/dist/commands/admin/coupons/redeem.js.map +1 -1
  18. package/dist/commands/auth/whoami.js +7 -1
  19. package/dist/commands/auth/whoami.js.map +1 -1
  20. package/dist/commands/billing/__tests__/status.test.js +79 -3
  21. package/dist/commands/billing/__tests__/status.test.js.map +1 -1
  22. package/dist/commands/billing/__tests__/subscribe.test.js +7 -1
  23. package/dist/commands/billing/__tests__/subscribe.test.js.map +1 -1
  24. package/dist/commands/billing/__tests__/top-up.test.js +473 -6
  25. package/dist/commands/billing/__tests__/top-up.test.js.map +1 -1
  26. package/dist/commands/billing/cancel.js +8 -3
  27. package/dist/commands/billing/cancel.js.map +1 -1
  28. package/dist/commands/billing/list-cards.js +7 -1
  29. package/dist/commands/billing/list-cards.js.map +1 -1
  30. package/dist/commands/billing/redeem-coupon.js +7 -1
  31. package/dist/commands/billing/redeem-coupon.js.map +1 -1
  32. package/dist/commands/billing/status.js +33 -1
  33. package/dist/commands/billing/status.js.map +1 -1
  34. package/dist/commands/billing/subscribe.js +7 -1
  35. package/dist/commands/billing/subscribe.js.map +1 -1
  36. package/dist/commands/billing/top-up.js +369 -7
  37. package/dist/commands/billing/top-up.js.map +1 -1
  38. package/dist/commands/compute/app/__tests__/status.test.js +1017 -0
  39. package/dist/commands/compute/app/__tests__/status.test.js.map +1 -0
  40. package/dist/commands/compute/app/create.js.map +1 -1
  41. package/dist/commands/compute/app/deploy.js +300 -68
  42. package/dist/commands/compute/app/deploy.js.map +1 -1
  43. package/dist/commands/compute/app/info.js +8 -2
  44. package/dist/commands/compute/app/info.js.map +1 -1
  45. package/dist/commands/compute/app/list.js +8 -2
  46. package/dist/commands/compute/app/list.js.map +1 -1
  47. package/dist/commands/compute/app/logs.js +11 -4
  48. package/dist/commands/compute/app/logs.js.map +1 -1
  49. package/dist/commands/compute/app/profile/set.js +11 -4
  50. package/dist/commands/compute/app/profile/set.js.map +1 -1
  51. package/dist/commands/compute/app/releases.js +8 -2
  52. package/dist/commands/compute/app/releases.js.map +1 -1
  53. package/dist/commands/compute/app/start.js +15 -8
  54. package/dist/commands/compute/app/start.js.map +1 -1
  55. package/dist/commands/compute/app/status.js +845 -0
  56. package/dist/commands/compute/app/status.js.map +1 -0
  57. package/dist/commands/compute/app/stop.js +15 -8
  58. package/dist/commands/compute/app/stop.js.map +1 -1
  59. package/dist/commands/compute/app/terminate.js +15 -8
  60. package/dist/commands/compute/app/terminate.js.map +1 -1
  61. package/dist/commands/compute/app/upgrade.js +297 -67
  62. package/dist/commands/compute/app/upgrade.js.map +1 -1
  63. package/dist/commands/compute/build/info.js +8 -2
  64. package/dist/commands/compute/build/info.js.map +1 -1
  65. package/dist/commands/compute/build/list.js +8 -2
  66. package/dist/commands/compute/build/list.js.map +1 -1
  67. package/dist/commands/compute/build/logs.js +8 -2
  68. package/dist/commands/compute/build/logs.js.map +1 -1
  69. package/dist/commands/compute/build/status.js +8 -2
  70. package/dist/commands/compute/build/status.js.map +1 -1
  71. package/dist/commands/compute/build/submit.js +8 -2
  72. package/dist/commands/compute/build/submit.js.map +1 -1
  73. package/dist/commands/compute/build/verify.js +8 -2
  74. package/dist/commands/compute/build/verify.js.map +1 -1
  75. package/dist/commands/compute/environment/set.js.map +1 -1
  76. package/dist/commands/compute/undelegate.js +11 -4
  77. package/dist/commands/compute/undelegate.js.map +1 -1
  78. package/dist/hooks/init/__tests__/version-check.test.js +32 -2
  79. package/dist/hooks/init/__tests__/version-check.test.js.map +1 -1
  80. package/dist/hooks/init/version-check.js +6 -1
  81. package/dist/hooks/init/version-check.js.map +1 -1
  82. package/package.json +2 -2
@@ -251,7 +251,7 @@ var commonFlags = {
251
251
  required: false,
252
252
  description: "Deployment environment to use",
253
253
  env: "ECLOUD_ENV",
254
- default: async () => getDefaultEnvironment() || (getBuildType2() === "dev" ? "sepolia-dev" : "sepolia")
254
+ default: async () => getDefaultEnvironment() || (getBuildType2() === "dev" ? "sepolia-dev" : "mainnet-alpha")
255
255
  }),
256
256
  "private-key": Flags.string({
257
257
  required: false,
@@ -281,6 +281,12 @@ var commonFlags = {
281
281
  nonce: Flags.string({
282
282
  required: false,
283
283
  description: 'Override transaction nonce (integer or "latest" to replace a stuck transaction)'
284
+ }),
285
+ "non-interactive": Flags.boolean({
286
+ required: false,
287
+ description: "Assume non-interactive mode: default safe prompts and error all-at-once on missing required inputs",
288
+ env: "ECLOUD_NON_INTERACTIVE",
289
+ default: false
284
290
  })
285
291
  };
286
292
  async function validateCommonFlags(flags, options) {
@@ -382,22 +388,299 @@ async function withTelemetry(command, action) {
382
388
  }
383
389
  }
384
390
 
391
+ // src/commands/billing/top-up.ts
392
+ import {
393
+ getBillingEnvironmentConfig,
394
+ getBuildType as getBuildType4,
395
+ requirePrivateKey as requirePrivateKey2,
396
+ addHexPrefix as addHexPrefix2
397
+ } from "@layr-labs/ecloud-sdk";
398
+ import { privateKeyToAccount as privateKeyToAccount4 } from "viem/accounts";
399
+
400
+ // src/x402/client.ts
401
+ import { randomBytes as randomBytes2 } from "crypto";
402
+
403
+ // src/x402/types.ts
404
+ var USDC_DOMAINS = {
405
+ "eip155:84532": { name: "USDC", version: "2" },
406
+ // Base Sepolia
407
+ "eip155:8453": { name: "USD Coin", version: "2" }
408
+ // Base mainnet
409
+ };
410
+ function usdcDomainForNetwork(network, extra) {
411
+ const extraName = typeof extra?.name === "string" ? extra.name : void 0;
412
+ const extraVersion = typeof extra?.version === "string" ? extra.version : void 0;
413
+ if (extraName && extraVersion) {
414
+ return { name: extraName, version: extraVersion };
415
+ }
416
+ const fromTable = USDC_DOMAINS[network];
417
+ if (fromTable) {
418
+ return { name: extraName ?? fromTable.name, version: extraVersion ?? fromTable.version };
419
+ }
420
+ throw new Error(
421
+ `Cannot determine USDC EIP-712 domain for unknown x402 network "${network}"; the challenge must supply extra.name and extra.version.`
422
+ );
423
+ }
424
+
425
+ // src/x402/client.ts
426
+ var DEFAULT_TIMEOUT_MS = 6e4;
427
+ var X402Error = class extends Error {
428
+ status;
429
+ constructor(message, status) {
430
+ super(message);
431
+ this.name = "X402Error";
432
+ this.status = status;
433
+ }
434
+ };
435
+ function chainIdFromNetwork(network) {
436
+ const m = /^eip155:(\d+)$/.exec(network);
437
+ if (!m) {
438
+ throw new X402Error(`Unsupported x402 network "${network}" (expected eip155:<chainId>)`);
439
+ }
440
+ return Number(m[1]);
441
+ }
442
+ function parseChallenge(body) {
443
+ const reqs = body?.accepts?.[0];
444
+ if (!reqs) {
445
+ throw new X402Error("x402 challenge had no payment requirements (accepts was empty)");
446
+ }
447
+ if (reqs.scheme !== "exact") {
448
+ throw new X402Error(`Unsupported x402 scheme "${reqs.scheme}" (expected "exact")`);
449
+ }
450
+ chainIdFromNetwork(reqs.network);
451
+ const paymentId = reqs.extra?.paymentId;
452
+ if (typeof paymentId !== "string" || paymentId.length === 0) {
453
+ throw new X402Error("x402 challenge requirements missing extra.paymentId");
454
+ }
455
+ return reqs;
456
+ }
457
+ var TRANSFER_WITH_AUTHORIZATION_TYPES = {
458
+ TransferWithAuthorization: [
459
+ { name: "from", type: "address" },
460
+ { name: "to", type: "address" },
461
+ { name: "value", type: "uint256" },
462
+ { name: "validAfter", type: "uint256" },
463
+ { name: "validBefore", type: "uint256" },
464
+ { name: "nonce", type: "bytes32" }
465
+ ]
466
+ };
467
+ async function buildSignedPayload(reqs, account, nowSeconds, randomNonce) {
468
+ const chainId = chainIdFromNetwork(reqs.network);
469
+ const domainInfo = usdcDomainForNetwork(reqs.network, reqs.extra);
470
+ const validAfter = nowSeconds - 600;
471
+ const validBefore = nowSeconds + 3600;
472
+ const nonce = randomNonce();
473
+ const authorization = {
474
+ from: account.address,
475
+ to: reqs.payTo,
476
+ value: reqs.amount,
477
+ // signed verbatim — never recomputed
478
+ validAfter: String(validAfter),
479
+ validBefore: String(validBefore),
480
+ nonce
481
+ };
482
+ const signature = await account.signTypedData({
483
+ domain: {
484
+ name: domainInfo.name,
485
+ version: domainInfo.version,
486
+ chainId,
487
+ verifyingContract: reqs.asset
488
+ },
489
+ types: TRANSFER_WITH_AUTHORIZATION_TYPES,
490
+ primaryType: "TransferWithAuthorization",
491
+ message: {
492
+ from: account.address,
493
+ to: reqs.payTo,
494
+ value: BigInt(reqs.amount),
495
+ validAfter: BigInt(validAfter),
496
+ validBefore: BigInt(validBefore),
497
+ nonce
498
+ }
499
+ });
500
+ return {
501
+ x402Version: 2,
502
+ payload: { signature, authorization },
503
+ accepted: reqs
504
+ // echoed verbatim; carries extra.paymentId
505
+ };
506
+ }
507
+ function encodePaymentHeader(payload) {
508
+ return Buffer.from(JSON.stringify(payload), "utf8").toString("base64");
509
+ }
510
+ function createNonce() {
511
+ return "0x" + randomBytes2(32).toString("hex");
512
+ }
513
+ function decodeReceipt(headerValue) {
514
+ if (!headerValue) return void 0;
515
+ try {
516
+ return JSON.parse(Buffer.from(headerValue, "base64").toString("utf8"));
517
+ } catch {
518
+ return void 0;
519
+ }
520
+ }
521
+ async function readBody(resp) {
522
+ const text = await resp.text();
523
+ try {
524
+ return text ? JSON.parse(text) : {};
525
+ } catch {
526
+ return { error: text };
527
+ }
528
+ }
529
+ function describeFetchError(err, url, timeoutMs) {
530
+ if (err instanceof X402Error) return err;
531
+ const name = err?.name;
532
+ if (name === "AbortError" || name === "TimeoutError") {
533
+ return new X402Error(`x402 request to ${url} timed out after ${timeoutMs}ms`);
534
+ }
535
+ const cause = err?.cause;
536
+ const code = cause?.code ?? err?.code;
537
+ const detail = [code, cause?.message ?? err?.message].filter(Boolean).join(": ") || String(err);
538
+ let hint = "";
539
+ switch (code) {
540
+ case "ENOTFOUND":
541
+ case "EAI_AGAIN":
542
+ hint = " \u2014 the platform host could not be resolved; check --api-url / ECLOUD_API_URL";
543
+ break;
544
+ case "ECONNREFUSED":
545
+ hint = " \u2014 the platform host refused the connection; is the API URL correct and reachable?";
546
+ break;
547
+ case "ETIMEDOUT":
548
+ case "ECONNRESET":
549
+ hint = " \u2014 the connection dropped; the host may be unreachable from this network";
550
+ break;
551
+ }
552
+ return new X402Error(`x402 request to ${url} failed: ${detail}${hint}`);
553
+ }
554
+ async function purchaseCreditsX402(opts) {
555
+ const doFetch = opts.fetchImpl ?? fetch;
556
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
557
+ const body = JSON.stringify({ amountCents: opts.amountCents });
558
+ const log = (msg) => {
559
+ if (opts.verbose) console.error(`[x402] ${msg}`);
560
+ };
561
+ const request = async (label, headers) => {
562
+ const controller = new AbortController();
563
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
564
+ const startedAt = Date.now();
565
+ log(`${label}: POST ${opts.url}`);
566
+ try {
567
+ const resp = await doFetch(opts.url, { method: "POST", headers, body, signal: controller.signal });
568
+ log(`${label}: \u2190 HTTP ${resp.status} (${Date.now() - startedAt}ms)`);
569
+ return resp;
570
+ } catch (err) {
571
+ log(
572
+ `${label}: transport error: ${err?.name ?? "Error"}: ${err?.message ?? err}` + (err?.cause ? ` (cause: ${err.cause.code ?? ""} ${err.cause.message ?? ""})` : "")
573
+ );
574
+ throw describeFetchError(err, opts.url, timeoutMs);
575
+ } finally {
576
+ clearTimeout(timer);
577
+ }
578
+ };
579
+ log(`endpoint ${opts.url} \u2014 requesting ${opts.amountCents} cents of credits`);
580
+ const challengeResp = await request("challenge", { "Content-Type": "application/json" });
581
+ if (challengeResp.status !== 402) {
582
+ const errBody = await readBody(challengeResp);
583
+ log(
584
+ `challenge: expected 402, got ${challengeResp.status}; body: ${JSON.stringify(errBody)}`
585
+ );
586
+ throw new X402Error(
587
+ errBody?.error ?? `x402 challenge failed (HTTP ${challengeResp.status})`,
588
+ challengeResp.status
589
+ );
590
+ }
591
+ const reqs = parseChallenge(await readBody(challengeResp));
592
+ log(
593
+ `challenge: scheme=${reqs.scheme} network=${reqs.network} asset=${reqs.asset} amount=${reqs.amount} payTo=${reqs.payTo} paymentId=${reqs.extra?.paymentId}`
594
+ );
595
+ const payload = await buildSignedPayload(
596
+ reqs,
597
+ opts.account,
598
+ Math.floor(Date.now() / 1e3),
599
+ createNonce
600
+ );
601
+ const header = encodePaymentHeader(payload);
602
+ log(
603
+ `settle: signed authorization from=${payload.payload.authorization.from} validBefore=${payload.payload.authorization.validBefore}; sending X-PAYMENT (${header.length} chars)`
604
+ );
605
+ const settleResp = await request("settle", {
606
+ "Content-Type": "application/json",
607
+ "X-PAYMENT": header
608
+ });
609
+ if (settleResp.status !== 201) {
610
+ const errBody = await readBody(settleResp);
611
+ log(`settle: expected 201, got ${settleResp.status}; body: ${JSON.stringify(errBody)}`);
612
+ const serverMsg = typeof errBody?.error === "string" ? `: ${errBody.error}` : "";
613
+ switch (settleResp.status) {
614
+ case 402:
615
+ throw new X402Error(`x402 payment verification failed or the pending purchase expired${serverMsg}`, 402);
616
+ case 404:
617
+ throw new X402Error(`x402 target not found, or the pending purchase expired${serverMsg}`, 404);
618
+ case 409:
619
+ throw new X402Error(`x402 amount or target mismatch with the pending purchase${serverMsg}`, 409);
620
+ case 502:
621
+ throw new X402Error(`x402 settlement failed at the payment facilitator${serverMsg}`, 502);
622
+ default:
623
+ throw new X402Error(`x402 settlement failed (HTTP ${settleResp.status})${serverMsg}`, settleResp.status);
624
+ }
625
+ }
626
+ const ok = await readBody(settleResp);
627
+ const receipt = decodeReceipt(settleResp.headers.get("x-payment-response"));
628
+ const txHash = ok.txHash ?? receipt?.transaction ?? "";
629
+ const targetAddress = typeof ok.appId === "string" ? ok.appId : typeof ok.creatorAddr === "string" ? ok.creatorAddr : void 0;
630
+ const targetType = typeof ok.appId === "string" ? "app" : typeof ok.creatorAddr === "string" ? "creator" : void 0;
631
+ log(
632
+ `settle: 201 \u2014 txHash=${txHash} creditedCents=${ok.creditedCents} target=${targetType ?? "?"}:${targetAddress ?? "?"}`
633
+ );
634
+ return {
635
+ txHash,
636
+ paymentId: ok.paymentId ?? reqs.extra?.paymentId ?? "",
637
+ creditedCents: typeof ok.creditedCents === "number" ? ok.creditedCents : opts.amountCents,
638
+ targetType,
639
+ targetAddress
640
+ };
641
+ }
642
+
385
643
  // src/commands/billing/top-up.ts
386
644
  var POLL_INTERVAL_MS = 5e3;
387
645
  var POLL_TIMEOUT_MS = 3 * 60 * 1e3;
646
+ function resolveX402Target(flags, walletAddress) {
647
+ if (flags.app && flags.creator) {
648
+ throw new Error("--app and --creator are mutually exclusive");
649
+ }
650
+ if (flags.app) return { type: "app", address: flags.app };
651
+ if (flags.creator) return { type: "creator", address: flags.creator };
652
+ return { type: "creator", address: walletAddress };
653
+ }
654
+ function buildX402Url(baseUrl, target) {
655
+ const base = baseUrl.replace(/\/+$/, "");
656
+ const segment = target.type === "app" ? "apps" : "creators";
657
+ return `${base}/${segment}/${target.address}/x402-credits`;
658
+ }
659
+ function resolveX402BaseUrl(flags) {
660
+ const fromFlag = flags["api-url"];
661
+ if (fromFlag) return fromFlag.replace(/\/+$/, "");
662
+ const { billingApiServerURL } = getBillingEnvironmentConfig(getBuildType4());
663
+ if (billingApiServerURL) return billingApiServerURL.replace(/\/+$/, "");
664
+ throw new Error(
665
+ "No billing API URL configured; pass --api-url or set ECLOUD_API_URL / ECLOUD_BILLING_API_URL."
666
+ );
667
+ }
388
668
  var BillingTopUp = class _BillingTopUp extends Command {
389
- static description = "Purchase EigenCompute credits with USDC or credit card";
669
+ static description = "Purchase EigenCompute credits with USDC, credit card, or x402";
390
670
  static examples = [
391
671
  "<%= config.bin %> billing top-up",
392
672
  "<%= config.bin %> billing top-up --method usdc --amount 50",
393
- "<%= config.bin %> billing top-up --method card --amount 25"
673
+ "<%= config.bin %> billing top-up --method card --amount 25",
674
+ "<%= config.bin %> billing top-up --method x402 --amount 50",
675
+ "<%= config.bin %> billing top-up --method x402 --amount 50 --app 0xApp...",
676
+ "<%= config.bin %> billing top-up --method x402 --amount 50 --creator 0xCreator..."
394
677
  ];
395
678
  static flags = {
396
679
  ...commonFlags,
397
680
  method: Flags2.string({
398
681
  required: false,
399
- description: "Payment method: usdc (on-chain) or card (credit card)",
400
- options: ["usdc", "card"]
682
+ description: "Payment method: usdc (on-chain), card (credit card), or x402 (USDC over x402)",
683
+ options: ["usdc", "card", "x402"]
401
684
  }),
402
685
  amount: Flags2.string({
403
686
  required: false,
@@ -418,6 +701,19 @@ var BillingTopUp = class _BillingTopUp extends Command {
418
701
  required: false,
419
702
  description: "Blockchain network for USDC payment: ethereum or base",
420
703
  options: ["ethereum", "base"]
704
+ }),
705
+ creator: Flags2.string({
706
+ required: false,
707
+ description: "x402: creator address to credit (defaults to your wallet). Mutually exclusive with --app."
708
+ }),
709
+ app: Flags2.string({
710
+ required: false,
711
+ description: "x402: app address to credit. Mutually exclusive with --creator."
712
+ }),
713
+ "api-url": Flags2.string({
714
+ required: false,
715
+ description: "x402: override the billing API base URL (defaults to the environment's billing API)",
716
+ env: "ECLOUD_API_URL"
421
717
  })
422
718
  };
423
719
  async run() {
@@ -450,11 +746,14 @@ ${chalk2.bold("Purchase EigenCompute credits")}`);
450
746
  message: "How would you like to pay?",
451
747
  choices: [
452
748
  { value: "card", name: "Credit card" },
453
- { value: "usdc", name: "USDC (on-chain)" }
749
+ { value: "usdc", name: "USDC (on-chain)" },
750
+ { value: "x402", name: "USDC (x402)" }
454
751
  ]
455
752
  });
456
753
  if (method === "usdc") {
457
754
  await this.handleUsdc(billing, flags, walletAddress, targetAccount, baselineTotal);
755
+ } else if (method === "x402") {
756
+ await this.handleX402(billing, flags, walletAddress, baselineTotal);
458
757
  } else {
459
758
  await this.handleCard(billing, flags, baselineTotal);
460
759
  }
@@ -568,6 +867,66 @@ ${chalk2.yellow(" No USDC in wallet.")}`);
568
867
  }
569
868
  await this.pollForCredits(billing, flags, baselineTotal, dollars);
570
869
  }
870
+ async handleX402(billing, flags, walletAddress, baselineTotal) {
871
+ const MINIMUM_DOLLARS = 5;
872
+ const target = resolveX402Target(
873
+ { creator: flags.creator, app: flags.app },
874
+ walletAddress
875
+ );
876
+ const targetIsSelf = target.address.toLowerCase() === walletAddress.toLowerCase();
877
+ const baseUrl = resolveX402BaseUrl({ "api-url": flags["api-url"] });
878
+ const url = buildX402Url(baseUrl, target);
879
+ if (flags.verbose) {
880
+ console.error(`[x402] billing API base URL: ${baseUrl}`);
881
+ console.error(`[x402] endpoint: ${url}`);
882
+ console.error(`[x402] target: ${target.type} ${target.address} (self=${targetIsSelf})`);
883
+ }
884
+ const amountStr = flags.amount ?? await input2({
885
+ message: `How many dollars of credits to purchase? (minimum: $${MINIMUM_DOLLARS})`,
886
+ validate: (val) => {
887
+ const n = parseInt(val, 10);
888
+ if (isNaN(n) || n <= 0) return "Enter a positive whole number";
889
+ if (n.toString() !== val.trim()) return "Enter a whole dollar amount (no cents)";
890
+ if (n < MINIMUM_DOLLARS) return `Minimum purchase is $${MINIMUM_DOLLARS}`;
891
+ return true;
892
+ }
893
+ });
894
+ const dollars = parseInt(amountStr, 10);
895
+ if (isNaN(dollars) || dollars < MINIMUM_DOLLARS) {
896
+ this.error(`Minimum purchase is $${MINIMUM_DOLLARS}`);
897
+ }
898
+ const amountCents = dollars * 100;
899
+ if (target.type === "app") {
900
+ this.log(` ${chalk2.bold("Crediting app:")} ${target.address}`);
901
+ } else if (!targetIsSelf) {
902
+ this.log(` ${chalk2.bold("Crediting creator:")} ${target.address}`);
903
+ }
904
+ const { key } = await requirePrivateKey2({ privateKey: flags["private-key"] });
905
+ const account = privateKeyToAccount4(addHexPrefix2(key));
906
+ this.log(`
907
+ Purchasing ${chalk2.bold(`$${dollars}`)} in credits over x402...`);
908
+ const result = await purchaseCreditsX402({
909
+ url,
910
+ amountCents,
911
+ account,
912
+ verbose: flags.verbose
913
+ });
914
+ this.log(` ${chalk2.green("\u2713")} x402 payment settled`);
915
+ this.log(` ${chalk2.bold("Transaction:")} ${result.txHash}`);
916
+ this.log(` ${chalk2.bold("Payment ID:")} ${result.paymentId}`);
917
+ this.log(
918
+ ` ${chalk2.bold("Credits added:")} ${chalk2.cyan(`$${(result.creditedCents / 100).toFixed(2)}`)}`
919
+ );
920
+ if (targetIsSelf) {
921
+ await this.pollForCredits(billing, flags, baselineTotal, dollars);
922
+ } else {
923
+ this.log(
924
+ `
925
+ Credits applied to ${target.type} ${target.address}. Check that account's balance with: ecloud billing status
926
+ `
927
+ );
928
+ }
929
+ }
571
930
  async pollForCredits(billing, flags, baselineTotal, amountPurchased) {
572
931
  this.log(chalk2.gray("\n Waiting for credits to appear..."));
573
932
  const startTime = Date.now();
@@ -623,6 +982,22 @@ vi.mock("@inquirer/prompts", () => ({
623
982
  vi.mock("open", () => ({
624
983
  default: vi.fn()
625
984
  }));
985
+ vi.mock("../../../x402/client", () => ({
986
+ purchaseCreditsX402: vi.fn()
987
+ }));
988
+ vi.mock("@layr-labs/ecloud-sdk", async (importOriginal) => {
989
+ const actual = await importOriginal();
990
+ return {
991
+ ...actual,
992
+ requirePrivateKey: vi.fn().mockResolvedValue({
993
+ key: "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80",
994
+ source: "test"
995
+ }),
996
+ getBillingEnvironmentConfig: vi.fn().mockReturnValue({
997
+ billingApiServerURL: "https://platform-dev.example"
998
+ })
999
+ };
1000
+ });
626
1001
  var WALLET_ADDRESS = "0x1234567890abcdef1234567890abcdef12345678";
627
1002
  var TX_HASH = "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890";
628
1003
  describe("ecloud billing top-up", () => {
@@ -949,5 +1324,97 @@ describe("ecloud billing top-up", () => {
949
1324
  expect(select3).not.toHaveBeenCalled();
950
1325
  expect(input3).not.toHaveBeenCalled();
951
1326
  });
1327
+ describe("x402 helpers", () => {
1328
+ const WALLET = "0x1234567890abcdef1234567890abcdef12345678";
1329
+ describe("resolveX402Target", () => {
1330
+ it("defaults to the caller's own creator address", () => {
1331
+ expect(resolveX402Target({}, WALLET)).toEqual({ type: "creator", address: WALLET });
1332
+ });
1333
+ it("honors --creator", () => {
1334
+ expect(resolveX402Target({ creator: "0xC" }, WALLET)).toEqual({ type: "creator", address: "0xC" });
1335
+ });
1336
+ it("honors --app", () => {
1337
+ expect(resolveX402Target({ app: "0xA" }, WALLET)).toEqual({ type: "app", address: "0xA" });
1338
+ });
1339
+ it("throws when both --app and --creator are given", () => {
1340
+ expect(() => resolveX402Target({ app: "0xA", creator: "0xC" }, WALLET)).toThrow(/mutually exclusive/i);
1341
+ });
1342
+ });
1343
+ describe("buildX402Url", () => {
1344
+ it("builds a creator URL and trims a trailing slash", () => {
1345
+ expect(buildX402Url("https://h/", { type: "creator", address: "0xC" })).toBe(
1346
+ "https://h/creators/0xC/x402-credits"
1347
+ );
1348
+ });
1349
+ it("builds an app URL", () => {
1350
+ expect(buildX402Url("https://h", { type: "app", address: "0xA" })).toBe(
1351
+ "https://h/apps/0xA/x402-credits"
1352
+ );
1353
+ });
1354
+ });
1355
+ describe("resolveX402BaseUrl", () => {
1356
+ it("prefers --api-url over the billing API config", () => {
1357
+ expect(resolveX402BaseUrl({ "api-url": "https://flag" })).toBe("https://flag");
1358
+ });
1359
+ it("trims a trailing slash from --api-url", () => {
1360
+ expect(resolveX402BaseUrl({ "api-url": "https://flag/" })).toBe("https://flag");
1361
+ });
1362
+ it("falls back to the billing API server URL", () => {
1363
+ expect(resolveX402BaseUrl({})).toBe("https://platform-dev.example");
1364
+ });
1365
+ });
1366
+ });
1367
+ describe("x402 method", () => {
1368
+ it("happy path: default creator target, 201 settle, prints tx + credits", async () => {
1369
+ mockBilling.getStatus.mockResolvedValueOnce({ subscriptionStatus: "active", remainingCredits: 10 }).mockResolvedValueOnce({ subscriptionStatus: "active", remainingCredits: 60 });
1370
+ purchaseCreditsX402.mockResolvedValue({
1371
+ txHash: TX_HASH,
1372
+ paymentId: "pay_abc",
1373
+ creditedCents: 5e3,
1374
+ targetType: "creator",
1375
+ targetAddress: WALLET_ADDRESS
1376
+ });
1377
+ const cmd = createCommand({ amount: "50", method: "x402" });
1378
+ const promise = cmd.run();
1379
+ for (let i = 0; i < 10; i++) await vi.advanceTimersByTimeAsync(5e3);
1380
+ await promise;
1381
+ const out = logOutput.join("\n");
1382
+ expect(purchaseCreditsX402).toHaveBeenCalledWith(
1383
+ expect.objectContaining({
1384
+ url: `https://platform-dev.example/creators/${WALLET_ADDRESS}/x402-credits`,
1385
+ amountCents: 5e3
1386
+ })
1387
+ );
1388
+ expect(out).toContain("x402 payment settled");
1389
+ expect(out).toContain(TX_HASH);
1390
+ expect(out).toContain("$50.00");
1391
+ });
1392
+ it("--app targets the apps route", async () => {
1393
+ mockBilling.getStatus.mockResolvedValue({ subscriptionStatus: "active", remainingCredits: 10 });
1394
+ purchaseCreditsX402.mockResolvedValue({
1395
+ txHash: TX_HASH,
1396
+ paymentId: "pay_abc",
1397
+ creditedCents: 5e3,
1398
+ targetType: "app",
1399
+ targetAddress: "0xApp"
1400
+ });
1401
+ const cmd = createCommand({ amount: "50", method: "x402", app: "0xApp" });
1402
+ await cmd.run();
1403
+ expect(purchaseCreditsX402).toHaveBeenCalledWith(
1404
+ expect.objectContaining({ url: "https://platform-dev.example/apps/0xApp/x402-credits" })
1405
+ );
1406
+ });
1407
+ it("rejects --app together with --creator", async () => {
1408
+ const cmd = createCommand({ amount: "50", method: "x402", app: "0xApp", creator: "0xC" });
1409
+ await expect(cmd.run()).rejects.toThrow(/mutually exclusive/i);
1410
+ expect(purchaseCreditsX402).not.toHaveBeenCalled();
1411
+ });
1412
+ it("rejects amount below $5", async () => {
1413
+ mockBilling.getStatus.mockResolvedValue({ subscriptionStatus: "active", remainingCredits: 10 });
1414
+ const cmd = createCommand({ amount: "3", method: "x402" });
1415
+ await expect(cmd.run()).rejects.toThrow(/Minimum purchase is \$5/);
1416
+ expect(purchaseCreditsX402).not.toHaveBeenCalled();
1417
+ });
1418
+ });
952
1419
  });
953
1420
  //# sourceMappingURL=top-up.test.js.map