Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions ts/docs/commands/x402/pay.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,11 @@ Sends the request. If the endpoint answers with a successful status (2xx), that

While it runs, `pay` prints progress lines starting with `⏳` on stderr (in JSON mode, `{"type":"activity",...}` lines); stdout carries only the result.

The response body is returned in `data.response` — parsed when the endpoint says it is JSON — or written to a new file with `--out`. Responses are limited to 10 MB.
`--body-file` sends the original file bytes, or stdin bytes with `--body-file -`, up to 1 MiB. It does not decode text or infer a Content-Type; use `--header "Content-Type: application/json"` (or the endpoint's required type) when needed. Stdin cannot also supply a secret such as `--password-stdin`. Inline `--body` remains a text string.

The response body is returned in `data.response` — parsed for `application/json` or a `+json` media type — or written byte-for-byte to a new file with `--out`. Binary media types such as `application/octet-stream`, images, audio, video, PDF, and archives require `--out`; without it the command returns `invalid_option` (exit 2). Text, missing Content-Type, and unrecognized media types use strict UTF-8 decoding; invalid UTF-8 returns `invalid_x402_response` instead of replacing bytes. `--out` bypasses text and JSON decoding. Responses are limited to 10 MB.

A response format error can occur **after payment has settled**. In that case `error.details` retains `paymentStatus: "settled"`, `settled: true`, the transaction hash, and `retryPayment: false`. Do not pay again to recover the response; reconcile with the provider using the transaction hash. Choose `--out` before requesting a binary resource.

**When a payment fails, read `error.details` before trying again.** `paymentStatus: "not_sent"` means nothing left the account; `"unknown"` means the CLI cannot tell, so treat it as possibly paid and reconcile first; `retryPayment: false` means do not pay again to recover. See [x402 and B.AI payment details](../../machine-interface.md#x402-and-bai-payment-details).

Expand Down Expand Up @@ -142,7 +146,7 @@ printf '%s' "$PW" | wallet-cli x402 pay https://x402-gateway.bankofai.io/provide

## Exit status

`0` success, including a free response · `1` execution failure (`no_matching_requirement`, `amount_exceeds_limit` — both before signing, `paymentStatus: "not_sent"`; `gasfree_insufficient_balance` / `gasfree_not_activated`, `permit2_allowance_required` / `approval_reset_required`, `fee_cap_exceeded`, `payer_mismatch`, `invalid_settlement`, `invalid_x402_response`, `response_too_large`, `provider_rate_limited` — HTTP 429; `provider_error` — any other failed request or payment, with `error.details.phase` and `httpStatus`; `missing_wallet_address`; `auth_required` — a payment needs signing and no `--password-stdin` was given; `auth_failed`, `timeout`) · `2` usage error (`output_exists` — the `--out` file exists; `gasfree_credentials_missing`; `invalid_option` — two exclusive options, `--decimals` without `--asset`, or `--wait` / `--wait-timeout`, which this command does not take because the facilitator settles the payment; `invalid_amount` — a limit that is not a positive amount; `invalid_value` — e.g. a malformed `--header`, an unsupported `--method`, or a `--gasfree-relay` URL that is not HTTPS).
`0` success, including a free response · `1` execution failure (`no_matching_requirement`, `amount_exceeds_limit` — both before signing, `paymentStatus: "not_sent"`; `gasfree_insufficient_balance` / `gasfree_not_activated`, `permit2_allowance_required` / `approval_reset_required`, `fee_cap_exceeded`, `payer_mismatch`, `invalid_settlement`, `invalid_x402_response`, `response_too_large`, `provider_rate_limited` — HTTP 429; `provider_error` — any other failed request or payment, with `error.details.phase` and `httpStatus`; `missing_wallet_address`; `auth_required` — a payment needs signing and no `--password-stdin` was given; `auth_failed`, `timeout`) · `2` usage error (`output_exists` — the `--out` file exists; `gasfree_credentials_missing`; `invalid_option` — a binary response without `--out`, two exclusive options, `--decimals` without `--asset`, or `--wait` / `--wait-timeout`, which this command does not take because the facilitator settles the payment; `invalid_amount` — a limit that is not a positive amount; `invalid_value` — e.g. a malformed `--header`, an unsupported `--method`, or a `--gasfree-relay` URL that is not HTTPS).

## See also

Expand Down
15 changes: 11 additions & 4 deletions ts/src/adapters/inbound/cli/commands/artifact.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { closeSync, constants, fstatSync, openSync, readFileSync } from "node:fs";
import { UsageError } from "../../../../domain/errors/index.js";
import { errorMessage, UsageError } from "../../../../domain/errors/index.js";

export function readBoundedTextFile(path: string, maxBytes: number, label: string): string {
let fd: number | undefined;
Expand All @@ -9,9 +9,13 @@ export function readBoundedTextFile(path: string, maxBytes: number, label: strin
constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0),
);
const stat = fstatSync(fd);
if (!stat.isFile()) throw new UsageError("invalid_value", `${label} must be a regular file`);
if (!stat.isFile())
throw new UsageError("invalid_value", errorMessage`${label} must be a regular file`);
if (stat.size > maxBytes)
throw new UsageError("invalid_value", `${label} exceeds the ${maxBytes}-byte limit`);
throw new UsageError(
"invalid_value",
errorMessage`${label} exceeds the ${maxBytes}-byte limit`,
);
// Trimmed, not stripped: a file saved by an editor, `echo` or `jq -r` always carries a
// trailing newline, and every current caller (hex, JSON) treats surrounding whitespace as
// insignificant — but the trim must stop at the edges. Removing *all* whitespace would
Expand All @@ -20,7 +24,10 @@ export function readBoundedTextFile(path: string, maxBytes: number, label: strin
return readFileSync(fd, "utf8").trim();
} catch (error) {
if (error instanceof UsageError) throw error;
throw new UsageError("invalid_value", `could not read ${label}: ${(error as Error).message}`);
throw new UsageError(
"invalid_value",
errorMessage`could not read ${label}: ${(error as Error).message}`,
);
} finally {
if (fd !== undefined) closeSync(fd);
}
Expand Down
28 changes: 17 additions & 11 deletions ts/src/adapters/inbound/cli/commands/contract.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { z } from "zod";
import { readFile } from "node:fs/promises";
import type { ChainSpec, FamilyBinding } from "../contracts/index.js";
import { UsageError } from "../../../../domain/errors/index.js";
import { errorMessage, UsageError } from "../../../../domain/errors/index.js";
import type { TronContractService } from "../../../../application/use-cases/tron/contract-service.js";
import type { DeployConstructorArgs } from "../../../../application/ports/chain/gateway-provider.js";
import type { EvmContractService } from "../../../../application/use-cases/evm/contract-service.js";
Expand Down Expand Up @@ -265,9 +265,9 @@ async function creationBytecode(input: { code?: string; codeFile?: string }): Pr
text = await readFile(input.codeFile, "utf8");
} catch (error) {
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
throw new UsageError("file_not_found", `code file not found: ${input.codeFile}`);
throw new UsageError("file_not_found", errorMessage`code file not found: ${input.codeFile}`);
}
throw new UsageError("invalid_value", `cannot read code file: ${input.codeFile}`);
throw new UsageError("invalid_value", errorMessage`cannot read code file: ${input.codeFile}`);
}
return checkedCreationCode(text, `--code-file ${input.codeFile}`);
}
Expand Down Expand Up @@ -309,7 +309,7 @@ function checkedCreationCode(raw: string, source: string): string {
if (code === "" || hex.length % 2 !== 0 || !/^[0-9a-fA-F]*$/.test(hex)) {
throw new UsageError(
"invalid_value",
`${source} must hold even-length hex creation bytecode, or exactly 0x to deploy empty code; it must not be empty`,
errorMessage`${source} must hold even-length hex creation bytecode, or exactly 0x to deploy empty code; it must not be empty`,
);
}
return /^0x/i.test(code) ? `0x${hex}` : code;
Expand Down Expand Up @@ -340,30 +340,30 @@ async function readArtifact(path: string): Promise<DeploySource> {
text = await readFile(path, "utf8");
} catch (error) {
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
throw new UsageError("file_not_found", `artifact not found: ${path}`);
throw new UsageError("file_not_found", errorMessage`artifact not found: ${path}`);
}
throw new UsageError("invalid_value", `cannot read artifact: ${path}`);
throw new UsageError("invalid_value", errorMessage`cannot read artifact: ${path}`);
}
let artifact: Record<string, any>;
try {
artifact = JSON.parse(text);
} catch {
throw new UsageError("invalid_value", `artifact is not valid JSON: ${path}`);
throw new UsageError("invalid_value", errorMessage`artifact is not valid JSON: ${path}`);
}
const bytecode =
artifact?.bytecode?.object ?? artifact?.bytecode ?? artifact?.evm?.bytecode?.object;
if (typeof bytecode !== "string") {
throw new UsageError(
"invalid_value",
`artifact has no creation bytecode: ${path} (looked at .bytecode.object, .bytecode and .evm.bytecode.object)`,
errorMessage`artifact has no creation bytecode: ${path} (looked at .bytecode.object, .bytecode and .evm.bytecode.object)`,
);
}
// solc emits "0x" for an interface or an abstract contract: a real artifact for something that
// cannot be deployed. Saying so beats letting an empty deployment reach the chain.
if (bytecode.replace(/^0x/, "") === "") {
throw new UsageError(
"invalid_value",
`artifact holds no deployable bytecode: ${path} — an interface or abstract contract cannot be deployed`,
errorMessage`artifact holds no deployable bytecode: ${path} — an interface or abstract contract cannot be deployed`,
);
}
return { bytecode, ...(artifact.abi === undefined ? {} : { abi: artifact.abi }) };
Expand Down Expand Up @@ -867,8 +867,14 @@ export const contractCreate2TronBinding = (svc: TronContractService): FamilyBind
} catch (error) {
const codeValue = (error as NodeJS.ErrnoException).code;
if (codeValue === "ENOENT")
throw new UsageError("file_not_found", `code file not found: ${input.codeFile}`);
throw new UsageError("invalid_value", `cannot read code file: ${input.codeFile}`);
throw new UsageError(
"file_not_found",
errorMessage`code file not found: ${input.codeFile}`,
);
throw new UsageError(
"invalid_value",
errorMessage`cannot read code file: ${input.codeFile}`,
);
}
}
return svc.create2(input.deployer, code!, input.salt);
Expand Down
6 changes: 3 additions & 3 deletions ts/src/adapters/inbound/cli/commands/sunpump/launch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { readFile } from "node:fs/promises";
import type { ChainSpec, FamilyBinding } from "../../contracts/command.js";
import type { SunPumpTokenLaunchService } from "../../../../../application/use-cases/tron/sunpump/token-launch-service.js";
import type { SunPumpLaunchImage } from "../../../../../application/use-cases/tron/sunpump/token-launch-service.js";
import { UsageError } from "../../../../../domain/errors/index.js";
import { errorMessage, UsageError } from "../../../../../domain/errors/index.js";
import { TextFormatters } from "../../render/index.js";

/**
Expand Down Expand Up @@ -146,9 +146,9 @@ async function imageOf(input: {
bytes = await readFile(path);
} catch (error) {
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
throw new UsageError("file_not_found", `image file not found: ${path}`);
throw new UsageError("file_not_found", errorMessage`image file not found: ${path}`);
}
throw new UsageError("invalid_value", `cannot read image file: ${path}`);
throw new UsageError("invalid_value", errorMessage`cannot read image file: ${path}`);
}
return {
image: { source: "file", path, bytes: bytes.byteLength, base64: bytes.toString("base64") },
Expand Down
7 changes: 4 additions & 3 deletions ts/src/adapters/inbound/cli/commands/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
selectLedgerPath,
} from "../../../../application/services/ledger-account.js";
import { ChainFamily, CHAIN_FAMILIES, FAMILIES } from "../../../../domain/family/index.js";
import { UsageError } from "../../../../domain/errors/index.js";
import { errorMessage, UsageError } from "../../../../domain/errors/index.js";
import { passwordPolicyErrors } from "../input/prompt/validators.js";
import { readBoundedTextFile } from "./artifact.js";
import { TextFormatters } from "../render/index.js";
Expand Down Expand Up @@ -97,12 +97,13 @@ const KEYSTORE_MAX_BYTES = 64 * 1024;
/** the parsed JSON of a keystore file. Distinguishes "no such file" from "not a keystore" so the
* caller learns which of the two mistakes they made. */
function readKeystoreFile(path: string): unknown {
if (!existsSync(path)) throw new UsageError("keystore_not_found", `no keystore file at ${path}`);
if (!existsSync(path))
throw new UsageError("keystore_not_found", errorMessage`no keystore file at ${path}`);
const raw = readBoundedTextFile(path, KEYSTORE_MAX_BYTES, "keystore file");
try {
return JSON.parse(raw) as unknown;
} catch {
throw new UsageError("invalid_keystore", `${path} is not valid JSON`);
throw new UsageError("invalid_keystore", errorMessage`${path} is not valid JSON`);
}
}

Expand Down
Loading
Loading