@layr-labs/ecloud-cli 1.0.0-devep7 → 1.0.0
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.
- package/VERSION +2 -2
- package/dist/commands/admin/admins/add.js +411 -0
- package/dist/commands/admin/admins/add.js.map +1 -0
- package/dist/commands/admin/admins/list.js +414 -0
- package/dist/commands/admin/admins/list.js.map +1 -0
- package/dist/commands/admin/admins/remove.js +409 -0
- package/dist/commands/admin/admins/remove.js.map +1 -0
- package/dist/commands/admin/coupons/create.js +424 -0
- package/dist/commands/admin/coupons/create.js.map +1 -0
- package/dist/commands/admin/coupons/deactivate.js +409 -0
- package/dist/commands/admin/coupons/deactivate.js.map +1 -0
- package/dist/commands/admin/coupons/get.js +418 -0
- package/dist/commands/admin/coupons/get.js.map +1 -0
- package/dist/commands/admin/coupons/list.js +442 -0
- package/dist/commands/admin/coupons/list.js.map +1 -0
- package/dist/commands/admin/coupons/redeem.js +413 -0
- package/dist/commands/admin/coupons/redeem.js.map +1 -0
- package/dist/commands/auth/whoami.js +7 -1
- package/dist/commands/auth/whoami.js.map +1 -1
- package/dist/commands/billing/__tests__/status.test.js +82 -4
- package/dist/commands/billing/__tests__/status.test.js.map +1 -1
- package/dist/commands/billing/__tests__/subscribe.test.js +10 -2
- package/dist/commands/billing/__tests__/subscribe.test.js.map +1 -1
- package/dist/commands/billing/__tests__/top-up.test.js +565 -13
- package/dist/commands/billing/__tests__/top-up.test.js.map +1 -1
- package/dist/commands/billing/cancel.js +11 -4
- package/dist/commands/billing/cancel.js.map +1 -1
- package/dist/commands/billing/list-cards.js +10 -2
- package/dist/commands/billing/list-cards.js.map +1 -1
- package/dist/commands/billing/redeem-coupon.js +421 -0
- package/dist/commands/billing/redeem-coupon.js.map +1 -0
- package/dist/commands/billing/status.js +36 -2
- package/dist/commands/billing/status.js.map +1 -1
- package/dist/commands/billing/subscribe.js +10 -2
- package/dist/commands/billing/subscribe.js.map +1 -1
- package/dist/commands/billing/top-up.js +392 -11
- package/dist/commands/billing/top-up.js.map +1 -1
- package/dist/commands/compute/app/__tests__/status.test.js +1017 -0
- package/dist/commands/compute/app/__tests__/status.test.js.map +1 -0
- package/dist/commands/compute/app/configure/tls.js +70 -30
- package/dist/commands/compute/app/configure/tls.js.map +1 -1
- package/dist/commands/compute/app/create.js.map +1 -1
- package/dist/commands/compute/app/deploy.js +301 -68
- package/dist/commands/compute/app/deploy.js.map +1 -1
- package/dist/commands/compute/app/info.js +8 -2
- package/dist/commands/compute/app/info.js.map +1 -1
- package/dist/commands/compute/app/list.js +8 -2
- package/dist/commands/compute/app/list.js.map +1 -1
- package/dist/commands/compute/app/logs.js +12 -4
- package/dist/commands/compute/app/logs.js.map +1 -1
- package/dist/commands/compute/app/profile/set.js +12 -4
- package/dist/commands/compute/app/profile/set.js.map +1 -1
- package/dist/commands/compute/app/releases.js +8 -2
- package/dist/commands/compute/app/releases.js.map +1 -1
- package/dist/commands/compute/app/start.js +16 -8
- package/dist/commands/compute/app/start.js.map +1 -1
- package/dist/commands/compute/app/status.js +845 -0
- package/dist/commands/compute/app/status.js.map +1 -0
- package/dist/commands/compute/app/stop.js +16 -8
- package/dist/commands/compute/app/stop.js.map +1 -1
- package/dist/commands/compute/app/terminate.js +16 -8
- package/dist/commands/compute/app/terminate.js.map +1 -1
- package/dist/commands/compute/app/upgrade.js +298 -67
- package/dist/commands/compute/app/upgrade.js.map +1 -1
- package/dist/commands/compute/build/info.js +9 -2
- package/dist/commands/compute/build/info.js.map +1 -1
- package/dist/commands/compute/build/list.js +9 -2
- package/dist/commands/compute/build/list.js.map +1 -1
- package/dist/commands/compute/build/logs.js +9 -2
- package/dist/commands/compute/build/logs.js.map +1 -1
- package/dist/commands/compute/build/status.js +9 -2
- package/dist/commands/compute/build/status.js.map +1 -1
- package/dist/commands/compute/build/submit.js +9 -2
- package/dist/commands/compute/build/submit.js.map +1 -1
- package/dist/commands/compute/build/verify.js +9 -2
- package/dist/commands/compute/build/verify.js.map +1 -1
- package/dist/commands/compute/environment/set.js.map +1 -1
- package/dist/commands/compute/undelegate.js +12 -4
- package/dist/commands/compute/undelegate.js.map +1 -1
- package/dist/hooks/init/__tests__/version-check.test.js +32 -2
- package/dist/hooks/init/__tests__/version-check.test.js.map +1 -1
- package/dist/hooks/init/version-check.js +6 -1
- package/dist/hooks/init/version-check.js.map +1 -1
- package/package.json +11 -2
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
createComputeModule,
|
|
12
12
|
createBillingModule,
|
|
13
13
|
createBuildModule,
|
|
14
|
+
createAdminModule,
|
|
14
15
|
getEnvironmentConfig as getEnvironmentConfig3,
|
|
15
16
|
requirePrivateKey
|
|
16
17
|
} from "@layr-labs/ecloud-sdk";
|
|
@@ -250,7 +251,7 @@ var commonFlags = {
|
|
|
250
251
|
required: false,
|
|
251
252
|
description: "Deployment environment to use",
|
|
252
253
|
env: "ECLOUD_ENV",
|
|
253
|
-
default: async () => getDefaultEnvironment() || (getBuildType2() === "dev" ? "sepolia-dev" : "
|
|
254
|
+
default: async () => getDefaultEnvironment() || (getBuildType2() === "dev" ? "sepolia-dev" : "mainnet-alpha")
|
|
254
255
|
}),
|
|
255
256
|
"private-key": Flags.string({
|
|
256
257
|
required: false,
|
|
@@ -280,6 +281,12 @@ var commonFlags = {
|
|
|
280
281
|
nonce: Flags.string({
|
|
281
282
|
required: false,
|
|
282
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
|
|
283
290
|
})
|
|
284
291
|
};
|
|
285
292
|
async function validateCommonFlags(flags, options) {
|
|
@@ -312,7 +319,8 @@ async function createBillingClient(flags) {
|
|
|
312
319
|
walletClient,
|
|
313
320
|
publicClient,
|
|
314
321
|
environment,
|
|
315
|
-
skipTelemetry: true
|
|
322
|
+
skipTelemetry: true,
|
|
323
|
+
privateKey
|
|
316
324
|
});
|
|
317
325
|
}
|
|
318
326
|
|
|
@@ -380,22 +388,299 @@ async function withTelemetry(command, action) {
|
|
|
380
388
|
}
|
|
381
389
|
}
|
|
382
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
|
+
|
|
383
643
|
// src/commands/billing/top-up.ts
|
|
384
644
|
var POLL_INTERVAL_MS = 5e3;
|
|
385
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
|
+
}
|
|
386
668
|
var BillingTopUp = class _BillingTopUp extends Command {
|
|
387
|
-
static description = "Purchase EigenCompute credits with USDC
|
|
669
|
+
static description = "Purchase EigenCompute credits with USDC, credit card, or x402";
|
|
388
670
|
static examples = [
|
|
389
671
|
"<%= config.bin %> billing top-up",
|
|
390
672
|
"<%= config.bin %> billing top-up --method usdc --amount 50",
|
|
391
|
-
"<%= 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..."
|
|
392
677
|
];
|
|
393
678
|
static flags = {
|
|
394
679
|
...commonFlags,
|
|
395
680
|
method: Flags2.string({
|
|
396
681
|
required: false,
|
|
397
|
-
description: "Payment method: usdc (on-chain)
|
|
398
|
-
options: ["usdc", "card"]
|
|
682
|
+
description: "Payment method: usdc (on-chain), card (credit card), or x402 (USDC over x402)",
|
|
683
|
+
options: ["usdc", "card", "x402"]
|
|
399
684
|
}),
|
|
400
685
|
amount: Flags2.string({
|
|
401
686
|
required: false,
|
|
@@ -411,6 +696,24 @@ var BillingTopUp = class _BillingTopUp extends Command {
|
|
|
411
696
|
default: "compute",
|
|
412
697
|
options: ["compute"],
|
|
413
698
|
env: "ECLOUD_PRODUCT_ID"
|
|
699
|
+
}),
|
|
700
|
+
chain: Flags2.string({
|
|
701
|
+
required: false,
|
|
702
|
+
description: "Blockchain network for USDC payment: ethereum or base",
|
|
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"
|
|
414
717
|
})
|
|
415
718
|
};
|
|
416
719
|
async run() {
|
|
@@ -443,25 +746,39 @@ ${chalk2.bold("Purchase EigenCompute credits")}`);
|
|
|
443
746
|
message: "How would you like to pay?",
|
|
444
747
|
choices: [
|
|
445
748
|
{ value: "card", name: "Credit card" },
|
|
446
|
-
{ value: "usdc", name: "USDC (on-chain)" }
|
|
749
|
+
{ value: "usdc", name: "USDC (on-chain)" },
|
|
750
|
+
{ value: "x402", name: "USDC (x402)" }
|
|
447
751
|
]
|
|
448
752
|
});
|
|
449
753
|
if (method === "usdc") {
|
|
450
754
|
await this.handleUsdc(billing, flags, walletAddress, targetAccount, baselineTotal);
|
|
755
|
+
} else if (method === "x402") {
|
|
756
|
+
await this.handleX402(billing, flags, walletAddress, baselineTotal);
|
|
451
757
|
} else {
|
|
452
758
|
await this.handleCard(billing, flags, baselineTotal);
|
|
453
759
|
}
|
|
454
760
|
});
|
|
455
761
|
}
|
|
456
762
|
async handleUsdc(billing, flags, walletAddress, targetAccount, baselineTotal) {
|
|
457
|
-
|
|
763
|
+
let selectedChain = "ethereum";
|
|
764
|
+
if (billing.hasBaseSupport()) {
|
|
765
|
+
selectedChain = flags.chain ?? await select2({
|
|
766
|
+
message: "Which network?",
|
|
767
|
+
choices: [
|
|
768
|
+
{ value: "ethereum", name: "Ethereum" },
|
|
769
|
+
{ value: "base", name: "Base" }
|
|
770
|
+
]
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
const onChainState = await billing.getTopUpInfo({ chain: selectedChain });
|
|
458
774
|
const { usdcBalance, minimumPurchase } = onChainState;
|
|
459
775
|
const balanceFormatted = formatUnits(usdcBalance, 6);
|
|
460
776
|
this.log(` ${chalk2.bold("USDC:")} ${balanceFormatted} USDC`);
|
|
461
777
|
if (usdcBalance === BigInt(0)) {
|
|
778
|
+
const networkName = selectedChain === "base" ? "Base Sepolia" : "Sepolia";
|
|
462
779
|
this.log(`
|
|
463
780
|
${chalk2.yellow(" No USDC in wallet.")}`);
|
|
464
|
-
this.log(` Send USDC on
|
|
781
|
+
this.log(` Send USDC on ${networkName} to: ${chalk2.cyan(walletAddress)}`);
|
|
465
782
|
this.log(` Then re-run: ${chalk2.cyan("ecloud billing top-up")}
|
|
466
783
|
`);
|
|
467
784
|
return;
|
|
@@ -494,7 +811,8 @@ ${chalk2.yellow(" No USDC in wallet.")}`);
|
|
|
494
811
|
Purchasing ${chalk2.bold(`$${amountFloat.toFixed(2)}`)} in credits...`);
|
|
495
812
|
const { txHash } = await billing.topUp({
|
|
496
813
|
amount: amountRaw,
|
|
497
|
-
account: targetAccount
|
|
814
|
+
account: targetAccount,
|
|
815
|
+
chain: selectedChain
|
|
498
816
|
});
|
|
499
817
|
this.log(` ${chalk2.green("\u2713")} Transaction confirmed: ${txHash}`);
|
|
500
818
|
await this.pollForCredits(billing, flags, baselineTotal, amountFloat);
|
|
@@ -549,6 +867,66 @@ ${chalk2.yellow(" No USDC in wallet.")}`);
|
|
|
549
867
|
}
|
|
550
868
|
await this.pollForCredits(billing, flags, baselineTotal, dollars);
|
|
551
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
|
+
}
|
|
552
930
|
async pollForCredits(billing, flags, baselineTotal, amountPurchased) {
|
|
553
931
|
this.log(chalk2.gray("\n Waiting for credits to appear..."));
|
|
554
932
|
const startTime = Date.now();
|
|
@@ -604,6 +982,22 @@ vi.mock("@inquirer/prompts", () => ({
|
|
|
604
982
|
vi.mock("open", () => ({
|
|
605
983
|
default: vi.fn()
|
|
606
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
|
+
});
|
|
607
1001
|
var WALLET_ADDRESS = "0x1234567890abcdef1234567890abcdef12345678";
|
|
608
1002
|
var TX_HASH = "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890";
|
|
609
1003
|
describe("ecloud billing top-up", () => {
|
|
@@ -619,8 +1013,10 @@ describe("ecloud billing top-up", () => {
|
|
|
619
1013
|
getTopUpInfo: vi.fn(),
|
|
620
1014
|
topUp: vi.fn(),
|
|
621
1015
|
getPaymentMethods: vi.fn(),
|
|
622
|
-
purchaseCredits: vi.fn()
|
|
1016
|
+
purchaseCredits: vi.fn(),
|
|
1017
|
+
hasBaseSupport: vi.fn()
|
|
623
1018
|
};
|
|
1019
|
+
mockBilling.hasBaseSupport.mockReturnValue(false);
|
|
624
1020
|
createBillingClient.mockResolvedValue(mockBilling);
|
|
625
1021
|
input3.mockResolvedValue("50");
|
|
626
1022
|
});
|
|
@@ -680,7 +1076,8 @@ describe("ecloud billing top-up", () => {
|
|
|
680
1076
|
expect(fullOutput).toContain("$60.00");
|
|
681
1077
|
expect(mockBilling.topUp).toHaveBeenCalledWith({
|
|
682
1078
|
amount: BigInt(5e7),
|
|
683
|
-
account: WALLET_ADDRESS
|
|
1079
|
+
account: WALLET_ADDRESS,
|
|
1080
|
+
chain: "ethereum"
|
|
684
1081
|
});
|
|
685
1082
|
});
|
|
686
1083
|
it("zero USDC balance: exits with fund wallet message", async () => {
|
|
@@ -715,7 +1112,8 @@ describe("ecloud billing top-up", () => {
|
|
|
715
1112
|
expect(fullOutput).toContain(targetAccount);
|
|
716
1113
|
expect(mockBilling.topUp).toHaveBeenCalledWith({
|
|
717
1114
|
amount: BigInt(5e7),
|
|
718
|
-
account: targetAccount
|
|
1115
|
+
account: targetAccount,
|
|
1116
|
+
chain: "ethereum"
|
|
719
1117
|
});
|
|
720
1118
|
});
|
|
721
1119
|
it("billing API poll timeout: shows timeout message", async () => {
|
|
@@ -758,6 +1156,68 @@ describe("ecloud billing top-up", () => {
|
|
|
758
1156
|
expect(fullOutput).toContain("Transaction confirmed");
|
|
759
1157
|
expect(fullOutput).toContain("Credits haven't appeared yet");
|
|
760
1158
|
});
|
|
1159
|
+
it("usdc: prompts for chain selection when Base is available", async () => {
|
|
1160
|
+
mockBilling.hasBaseSupport.mockReturnValue(true);
|
|
1161
|
+
setupOnChainState();
|
|
1162
|
+
mockBilling.topUp.mockResolvedValue({ txHash: TX_HASH, walletAddress: WALLET_ADDRESS });
|
|
1163
|
+
mockBilling.getStatus.mockResolvedValueOnce({ subscriptionStatus: "active", remainingCredits: 10 }).mockResolvedValueOnce({ subscriptionStatus: "active", remainingCredits: 60 });
|
|
1164
|
+
select3.mockResolvedValue("base");
|
|
1165
|
+
const cmd = createCommand({ amount: "50", method: "usdc" });
|
|
1166
|
+
const promise = cmd.run();
|
|
1167
|
+
for (let i = 0; i < 10; i++) {
|
|
1168
|
+
await vi.advanceTimersByTimeAsync(5e3);
|
|
1169
|
+
}
|
|
1170
|
+
await promise;
|
|
1171
|
+
expect(select3).toHaveBeenCalledWith(
|
|
1172
|
+
expect.objectContaining({
|
|
1173
|
+
message: "Which network?",
|
|
1174
|
+
choices: expect.arrayContaining([
|
|
1175
|
+
expect.objectContaining({ value: "base" }),
|
|
1176
|
+
expect.objectContaining({ value: "ethereum" })
|
|
1177
|
+
])
|
|
1178
|
+
})
|
|
1179
|
+
);
|
|
1180
|
+
expect(mockBilling.getTopUpInfo).toHaveBeenCalledWith({ chain: "base" });
|
|
1181
|
+
expect(mockBilling.topUp).toHaveBeenCalledWith({
|
|
1182
|
+
amount: BigInt(5e7),
|
|
1183
|
+
account: WALLET_ADDRESS,
|
|
1184
|
+
chain: "base"
|
|
1185
|
+
});
|
|
1186
|
+
});
|
|
1187
|
+
it("usdc: skips chain prompt when Base is not configured", async () => {
|
|
1188
|
+
mockBilling.hasBaseSupport.mockReturnValue(false);
|
|
1189
|
+
setupOnChainState();
|
|
1190
|
+
mockBilling.topUp.mockResolvedValue({ txHash: TX_HASH, walletAddress: WALLET_ADDRESS });
|
|
1191
|
+
mockBilling.getStatus.mockResolvedValueOnce({ subscriptionStatus: "active", remainingCredits: 10 }).mockResolvedValueOnce({ subscriptionStatus: "active", remainingCredits: 60 });
|
|
1192
|
+
const cmd = createCommand({ amount: "50", method: "usdc" });
|
|
1193
|
+
const promise = cmd.run();
|
|
1194
|
+
for (let i = 0; i < 10; i++) {
|
|
1195
|
+
await vi.advanceTimersByTimeAsync(5e3);
|
|
1196
|
+
}
|
|
1197
|
+
await promise;
|
|
1198
|
+
expect(mockBilling.topUp).toHaveBeenCalledWith({
|
|
1199
|
+
amount: BigInt(5e7),
|
|
1200
|
+
account: WALLET_ADDRESS,
|
|
1201
|
+
chain: "ethereum"
|
|
1202
|
+
});
|
|
1203
|
+
});
|
|
1204
|
+
it("usdc: --chain flag skips network prompt", async () => {
|
|
1205
|
+
mockBilling.hasBaseSupport.mockReturnValue(true);
|
|
1206
|
+
setupOnChainState();
|
|
1207
|
+
mockBilling.topUp.mockResolvedValue({ txHash: TX_HASH, walletAddress: WALLET_ADDRESS });
|
|
1208
|
+
mockBilling.getStatus.mockResolvedValueOnce({ subscriptionStatus: "active", remainingCredits: 10 }).mockResolvedValueOnce({ subscriptionStatus: "active", remainingCredits: 60 });
|
|
1209
|
+
const cmd = createCommand({ amount: "50", method: "usdc", chain: "base" });
|
|
1210
|
+
const promise = cmd.run();
|
|
1211
|
+
for (let i = 0; i < 10; i++) {
|
|
1212
|
+
await vi.advanceTimersByTimeAsync(5e3);
|
|
1213
|
+
}
|
|
1214
|
+
await promise;
|
|
1215
|
+
expect(mockBilling.topUp).toHaveBeenCalledWith({
|
|
1216
|
+
amount: BigInt(5e7),
|
|
1217
|
+
account: WALLET_ADDRESS,
|
|
1218
|
+
chain: "base"
|
|
1219
|
+
});
|
|
1220
|
+
});
|
|
761
1221
|
it("credit card: charges selected card on file", async () => {
|
|
762
1222
|
mockBilling.getStatus.mockResolvedValueOnce({ subscriptionStatus: "active", remainingCredits: 10 }).mockResolvedValueOnce({ subscriptionStatus: "active", remainingCredits: 35 });
|
|
763
1223
|
mockBilling.getPaymentMethods.mockResolvedValue({
|
|
@@ -864,5 +1324,97 @@ describe("ecloud billing top-up", () => {
|
|
|
864
1324
|
expect(select3).not.toHaveBeenCalled();
|
|
865
1325
|
expect(input3).not.toHaveBeenCalled();
|
|
866
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
|
+
});
|
|
867
1419
|
});
|
|
868
1420
|
//# sourceMappingURL=top-up.test.js.map
|