@zeroxyz/cli 0.0.6 → 0.0.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 (2) hide show
  1. package/dist/index.js +163 -72
  2. package/package.json +6 -2
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@ import { Command as Command8 } from "commander";
6
6
  // package.json
7
7
  var package_default = {
8
8
  name: "@zeroxyz/cli",
9
- version: "0.0.6",
9
+ version: "0.0.8",
10
10
  type: "module",
11
11
  bin: {
12
12
  zero: "dist/index.js",
@@ -26,8 +26,9 @@ var package_default = {
26
26
  dev: "tsx src/index.ts",
27
27
  cli: "ZERO_API_URL=http://localhost:1111 tsx src/index.ts",
28
28
  "test:integration": "vitest run --project integration",
29
+ "test:online": "vitest run --project online",
29
30
  "test:unit": "vitest run --project unit",
30
- test: "pnpm run test:integration",
31
+ test: "pnpm run test:unit && pnpm run test:integration",
31
32
  typecheck: "tsc"
32
33
  },
33
34
  dependencies: {
@@ -43,7 +44,10 @@ var package_default = {
43
44
  zod: "^4.3.5"
44
45
  },
45
46
  devDependencies: {
47
+ "@hono/node-server": "^1.19.13",
46
48
  "@types/node": "^25.0.7",
49
+ "@x402/hono": "^2.9.0",
50
+ hono: "^4.12.12",
47
51
  tsup: "^8.5.1",
48
52
  tsx: "^4.21.0",
49
53
  typescript: "^5.9.3",
@@ -105,7 +109,7 @@ var configCommand = (_appContext) => new Command("config").description("View or
105
109
  import { Command as Command2 } from "commander";
106
110
  var detectPaymentRequirement = (headers, status) => {
107
111
  if (status !== 402) return null;
108
- const x402Header = headers.get("x-payment-required");
112
+ const x402Header = headers.get("payment-required") ?? headers.get("x-payment-required");
109
113
  if (x402Header) {
110
114
  try {
111
115
  const decoded = JSON.parse(
@@ -235,39 +239,48 @@ var fetchCommand = (appContext2) => new Command2("fetch").description("Fetch a c
235
239
 
236
240
  // src/commands/get-command.ts
237
241
  import { Command as Command3 } from "commander";
238
- var getCommand = (appContext2) => new Command3("get").description("Get details for a capability from the last search").argument("<position>", "Position number from search results").action(async (positionStr) => {
242
+ var getCommand = (appContext2) => new Command3("get").description(
243
+ "Get details for a capability by position from last search, or by slug"
244
+ ).argument(
245
+ "<identifier>",
246
+ "Position number from search results, or a capability slug"
247
+ ).action(async (identifier) => {
239
248
  try {
240
249
  const { analyticsService, apiService, stateService } = appContext2.services;
241
- const position = Number.parseInt(positionStr, 10);
242
- if (Number.isNaN(position) || position < 1) {
243
- console.error("Position must be a positive integer");
244
- process.exitCode = 1;
245
- return;
246
- }
247
- const lastSearch = stateService.loadLastSearch();
248
- if (!lastSearch) {
249
- console.error("No recent search found. Run `zero search` first.");
250
- process.exitCode = 1;
251
- return;
252
- }
253
- const entry = lastSearch.capabilities.find(
254
- (c) => c.position === position
255
- );
256
- if (!entry) {
257
- console.error(
258
- `No capability at position ${position}. Positions: ${lastSearch.capabilities.map((c) => c.position).join(", ")}`
250
+ const position = Number.parseInt(identifier, 10);
251
+ const isPosition = !Number.isNaN(position) && position >= 1;
252
+ let capabilityId;
253
+ let searchId;
254
+ if (isPosition) {
255
+ const lastSearch = stateService.loadLastSearch();
256
+ if (!lastSearch) {
257
+ console.error("No recent search found. Run `zero search` first.");
258
+ process.exitCode = 1;
259
+ return;
260
+ }
261
+ const entry = lastSearch.capabilities.find(
262
+ (c) => c.position === position
259
263
  );
260
- process.exitCode = 1;
261
- return;
264
+ if (!entry) {
265
+ console.error(
266
+ `No capability at position ${position}. Positions: ${lastSearch.capabilities.map((c) => c.position).join(", ")}`
267
+ );
268
+ process.exitCode = 1;
269
+ return;
270
+ }
271
+ capabilityId = entry.id;
272
+ searchId = lastSearch.searchId;
273
+ } else {
274
+ capabilityId = identifier;
262
275
  }
263
276
  const capability = await apiService.getCapability(
264
- entry.id,
265
- lastSearch.searchId
277
+ capabilityId,
278
+ searchId
266
279
  );
267
280
  console.log(JSON.stringify(capability, null, 2));
268
281
  analyticsService.capture("capability_viewed", {
269
- capabilityId: entry.id,
270
- position
282
+ capabilityId,
283
+ ...isPosition ? { position } : {}
271
284
  });
272
285
  } catch (err) {
273
286
  console.error(err instanceof Error ? err.message : "Get failed");
@@ -920,7 +933,7 @@ import {
920
933
  encodePaymentSignatureHeader
921
934
  } from "@x402/core/http";
922
935
  import { ExactEvmScheme } from "@x402/evm/exact/client";
923
- import { Challenge } from "mppx";
936
+ import { Challenge, Receipt } from "mppx";
924
937
  import { Mppx, tempo } from "mppx/client";
925
938
  import {
926
939
  createPublicClient,
@@ -928,11 +941,16 @@ import {
928
941
  formatUnits,
929
942
  http
930
943
  } from "viem";
931
- import { base } from "viem/chains";
944
+ import { base, baseSepolia } from "viem/chains";
932
945
  var USDC_BASE = "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913";
946
+ var USDC_BASE_SEPOLIA = "0x036CbD53842c5426634e7929541eC2318f3dCF7e";
933
947
  var USDC_TEMPO = "0x20c000000000000000000000b9537d11c60e8b50";
948
+ var PATHUSD_TEMPO = "0x20c0000000000000000000000000000000000000";
934
949
  var BASE_CHAIN_ID = 8453;
935
950
  var TEMPO_CHAIN_ID = 4217;
951
+ var TEMPO_TESTNET_CHAIN_ID = 42431;
952
+ var X402_PAYMENT_MAX_ATTEMPTS = 6;
953
+ var X402_PAYMENT_RETRY_DELAY_MS = 300;
936
954
  var calculateBuffer = (baseBalance) => {
937
955
  const twentyFivePercent = baseBalance / 4n;
938
956
  const twoDollars = 2000000n;
@@ -946,6 +964,14 @@ var tempoChain = {
946
964
  default: { http: ["https://rpc.tempo.xyz"] }
947
965
  }
948
966
  };
967
+ var tempoTestnetChain = {
968
+ id: TEMPO_TESTNET_CHAIN_ID,
969
+ name: "Tempo Testnet",
970
+ nativeCurrency: { name: "USD", symbol: "USD", decimals: 18 },
971
+ rpcUrls: {
972
+ default: { http: ["https://rpc.moderato.tempo.xyz"] }
973
+ }
974
+ };
949
975
  var ERC20_BALANCE_ABI = [
950
976
  {
951
977
  inputs: [{ name: "account", type: "address" }],
@@ -1044,47 +1070,84 @@ var PaymentService = class {
1044
1070
  }
1045
1071
  throw new Error("Unrecognized 402 payment protocol");
1046
1072
  };
1047
- payX402 = async (url, request, raw, maxPay) => {
1048
- const headerValue = Buffer.from(JSON.stringify(raw)).toString("base64");
1049
- const paymentRequired = decodePaymentRequiredHeader(headerValue);
1050
- const requirement = paymentRequired.accepts[0];
1051
- if (!requirement) {
1052
- throw new Error("No accepted payment methods in x402 challenge");
1073
+ toX402RawChallenge = (headerValue) => {
1074
+ try {
1075
+ return JSON.parse(Buffer.from(headerValue, "base64").toString("utf8"));
1076
+ } catch {
1077
+ return { encoded: headerValue };
1053
1078
  }
1054
- const amountUsdc = formatUnits(BigInt(requirement.amount), 6);
1055
- if (maxPay && Number.parseFloat(amountUsdc) > Number.parseFloat(maxPay)) {
1056
- throw new Error(
1057
- `Payment of ${amountUsdc} USDC exceeds --max-pay ${maxPay}`
1058
- );
1079
+ };
1080
+ toX402HeaderValue = (raw) => {
1081
+ const encoded = raw?.encoded;
1082
+ if (typeof encoded === "string" && encoded.length > 0) {
1083
+ return encoded;
1059
1084
  }
1085
+ return Buffer.from(JSON.stringify(raw)).toString("base64");
1086
+ };
1087
+ payX402 = async (url, request, raw, maxPay) => {
1060
1088
  const client = this.getX402Client();
1061
- const paymentPayload = await client.createPaymentPayload(paymentRequired);
1062
- const signatureHeader = encodePaymentSignatureHeader(paymentPayload);
1063
- const retryResponse = await fetch(url, {
1064
- method: request.method,
1065
- headers: {
1066
- ...request.headers,
1067
- "PAYMENT-SIGNATURE": signatureHeader
1068
- },
1069
- body: request.body
1070
- });
1071
- let txHash = null;
1072
- const paymentResponseHeader = retryResponse.headers.get("payment-response");
1073
- if (paymentResponseHeader) {
1074
- try {
1075
- const settlement = decodePaymentResponseHeader(paymentResponseHeader);
1076
- txHash = settlement.transaction ?? null;
1077
- } catch {
1089
+ let currentRaw = raw;
1090
+ let amountUsdc = "0";
1091
+ for (let attempt = 1; attempt <= X402_PAYMENT_MAX_ATTEMPTS; attempt++) {
1092
+ const headerValue = this.toX402HeaderValue(currentRaw);
1093
+ const paymentRequired = decodePaymentRequiredHeader(headerValue);
1094
+ const requirement = paymentRequired.accepts[0];
1095
+ if (!requirement) {
1096
+ throw new Error("No accepted payment methods in x402 challenge");
1097
+ }
1098
+ amountUsdc = formatUnits(BigInt(requirement.amount), 6);
1099
+ if (maxPay && Number.parseFloat(amountUsdc) > Number.parseFloat(maxPay)) {
1100
+ throw new Error(
1101
+ `Payment of ${amountUsdc} USDC exceeds --max-pay ${maxPay}`
1102
+ );
1078
1103
  }
1104
+ const paymentPayload = await client.createPaymentPayload(paymentRequired);
1105
+ const signatureHeader = encodePaymentSignatureHeader(paymentPayload);
1106
+ const retryResponse = await fetch(url, {
1107
+ method: request.method,
1108
+ headers: {
1109
+ ...request.headers,
1110
+ "PAYMENT-SIGNATURE": signatureHeader
1111
+ },
1112
+ body: request.body
1113
+ });
1114
+ if (retryResponse.status === 402) {
1115
+ if (attempt >= X402_PAYMENT_MAX_ATTEMPTS) {
1116
+ throw new Error(
1117
+ `x402 payment failed after ${X402_PAYMENT_MAX_ATTEMPTS} attempts`
1118
+ );
1119
+ }
1120
+ const nextHeader = retryResponse.headers.get("payment-required") ?? retryResponse.headers.get("x-payment-required");
1121
+ if (!nextHeader) {
1122
+ throw new Error(
1123
+ "x402 payment retry returned 402 without payment-required header"
1124
+ );
1125
+ }
1126
+ currentRaw = this.toX402RawChallenge(nextHeader);
1127
+ await new Promise(
1128
+ (resolve) => setTimeout(resolve, attempt * X402_PAYMENT_RETRY_DELAY_MS)
1129
+ );
1130
+ continue;
1131
+ }
1132
+ let txHash = null;
1133
+ const paymentResponseHeader = retryResponse.headers.get("payment-response") ?? retryResponse.headers.get("x-payment-response");
1134
+ if (paymentResponseHeader) {
1135
+ try {
1136
+ const settlement = decodePaymentResponseHeader(paymentResponseHeader);
1137
+ txHash = settlement.transaction ?? null;
1138
+ } catch {
1139
+ }
1140
+ }
1141
+ return {
1142
+ response: retryResponse,
1143
+ protocol: "x402",
1144
+ chain: "base",
1145
+ txHash,
1146
+ amount: amountUsdc,
1147
+ asset: "USDC"
1148
+ };
1079
1149
  }
1080
- return {
1081
- response: retryResponse,
1082
- protocol: "x402",
1083
- chain: "base",
1084
- txHash,
1085
- amount: amountUsdc,
1086
- asset: "USDC"
1087
- };
1150
+ throw new Error("x402 payment failed");
1088
1151
  };
1089
1152
  payMpp = async (url, request, raw, maxPay) => {
1090
1153
  const wwwAuth = raw?.["www-authenticate"] ?? "";
@@ -1093,15 +1156,25 @@ var PaymentService = class {
1093
1156
  headers: { "www-authenticate": wwwAuth }
1094
1157
  });
1095
1158
  const challenge = Challenge.fromResponse(challengeResponse);
1096
- const amountRaw = challenge.request.amount;
1159
+ const challengeRequest = challenge.request;
1160
+ const amountRaw = challengeRequest.amount;
1097
1161
  const amountUsdc = formatUnits(BigInt(amountRaw), 6);
1098
1162
  if (maxPay && Number.parseFloat(amountUsdc) > Number.parseFloat(maxPay)) {
1099
1163
  throw new Error(
1100
1164
  `Payment of ${amountUsdc} USDC exceeds --max-pay ${maxPay}`
1101
1165
  );
1102
1166
  }
1103
- const tempoBalance = await this.getBalanceRaw("tempo");
1167
+ const methodDetails = challengeRequest.methodDetails;
1168
+ const challengeChainId = challengeRequest.chainId ?? methodDetails?.chainId;
1169
+ const isTestnet = challengeChainId === TEMPO_TESTNET_CHAIN_ID;
1170
+ const balanceChain = isTestnet ? "tempo-testnet" : "tempo";
1171
+ const tempoBalance = await this.getBalanceRaw(balanceChain);
1104
1172
  if (tempoBalance < BigInt(amountRaw)) {
1173
+ if (isTestnet) {
1174
+ throw new Error(
1175
+ `Insufficient pathUSD on Tempo testnet: have ${formatUnits(tempoBalance, 6)}, need ${amountUsdc}. Fund your wallet with the Tempo testnet faucet: https://docs.tempo.xyz/quickstart/faucet`
1176
+ );
1177
+ }
1105
1178
  await this.bridgeToTempo(BigInt(amountRaw));
1106
1179
  }
1107
1180
  const mppx = this.getMppxClient();
@@ -1115,24 +1188,42 @@ var PaymentService = class {
1115
1188
  },
1116
1189
  body: request.body
1117
1190
  });
1191
+ let txHash = null;
1192
+ try {
1193
+ const receipt = Receipt.fromResponse(retryResponse);
1194
+ txHash = receipt.reference ?? null;
1195
+ } catch {
1196
+ }
1118
1197
  return {
1119
1198
  response: retryResponse,
1120
1199
  protocol: "mpp",
1121
1200
  chain: "tempo",
1122
- txHash: null,
1123
- // MPP pull mode — server broadcasts, tx hash not returned in response
1201
+ txHash,
1124
1202
  amount: amountUsdc,
1125
1203
  asset: "USDC"
1126
1204
  };
1127
1205
  };
1206
+ resolveChainConfig = (chain) => {
1207
+ switch (chain) {
1208
+ case "base":
1209
+ return { viemChain: base, token: USDC_BASE };
1210
+ case "base-sepolia":
1211
+ return { viemChain: baseSepolia, token: USDC_BASE_SEPOLIA };
1212
+ case "tempo":
1213
+ return { viemChain: tempoChain, token: USDC_TEMPO };
1214
+ case "tempo-testnet":
1215
+ return { viemChain: tempoTestnetChain, token: PATHUSD_TEMPO };
1216
+ }
1217
+ };
1128
1218
  getBalanceRaw = async (chain) => {
1129
1219
  if (!this.account) return 0n;
1220
+ const { viemChain, token } = this.resolveChainConfig(chain);
1130
1221
  const client = createPublicClient({
1131
- chain: chain === "base" ? base : tempoChain,
1222
+ chain: viemChain,
1132
1223
  transport: http()
1133
1224
  });
1134
1225
  const balance = await client.readContract({
1135
- address: chain === "base" ? USDC_BASE : USDC_TEMPO,
1226
+ address: token,
1136
1227
  abi: ERC20_BALANCE_ABI,
1137
1228
  functionName: "balanceOf",
1138
1229
  args: [this.account.address]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeroxyz/cli",
3
- "version": "0.0.6",
3
+ "version": "0.0.8",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "zero": "dist/index.js",
@@ -20,8 +20,9 @@
20
20
  "dev": "tsx src/index.ts",
21
21
  "cli": "ZERO_API_URL=http://localhost:1111 tsx src/index.ts",
22
22
  "test:integration": "vitest run --project integration",
23
+ "test:online": "vitest run --project online",
23
24
  "test:unit": "vitest run --project unit",
24
- "test": "pnpm run test:integration",
25
+ "test": "pnpm run test:unit && pnpm run test:integration",
25
26
  "typecheck": "tsc"
26
27
  },
27
28
  "dependencies": {
@@ -37,7 +38,10 @@
37
38
  "zod": "^4.3.5"
38
39
  },
39
40
  "devDependencies": {
41
+ "@hono/node-server": "^1.19.13",
40
42
  "@types/node": "^25.0.7",
43
+ "@x402/hono": "^2.9.0",
44
+ "hono": "^4.12.12",
41
45
  "tsup": "^8.5.1",
42
46
  "tsx": "^4.21.0",
43
47
  "typescript": "^5.9.3",