@aibtc/mcp-server 1.58.0 → 1.59.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.
Files changed (258) hide show
  1. package/dist/index.js +18 -5
  2. package/dist/services/credentials.js +7 -2
  3. package/dist/services/defi.service.js +23 -11
  4. package/dist/services/hiro-api.d.ts +10 -0
  5. package/dist/services/hiro-api.js +30 -8
  6. package/dist/services/nft.service.js +14 -5
  7. package/dist/services/pillar-api.service.d.ts +8 -0
  8. package/dist/services/pillar-api.service.js +13 -1
  9. package/dist/services/x402.service.js +45 -13
  10. package/dist/tools/bitcoin.tools.js +14 -0
  11. package/dist/tools/endpoint.tools.js +3 -1
  12. package/dist/tools/inbox.tools.js +9 -12
  13. package/dist/tools/news.tools.js +9 -12
  14. package/dist/tools/pillar-direct.tools.js +29 -22
  15. package/dist/tools/sbtc.tools.js +2 -9
  16. package/dist/tools/scaffold.tools.js +9 -14
  17. package/dist/tools/stacking-lottery.tools.js +8 -5
  18. package/dist/tools/yield-dashboard.tools.js +10 -14
  19. package/dist/utils/encryption.js +2 -0
  20. package/package.json +10 -5
  21. package/skill/SKILL.md +1 -1
  22. package/skill/references/genesis-lifecycle.md +105 -92
  23. package/dist/config/bitcoin-constants.d.ts.map +0 -1
  24. package/dist/config/bitcoin-constants.js.map +0 -1
  25. package/dist/config/caip.d.ts.map +0 -1
  26. package/dist/config/caip.js.map +0 -1
  27. package/dist/config/competition.d.ts.map +0 -1
  28. package/dist/config/competition.js.map +0 -1
  29. package/dist/config/contracts.d.ts.map +0 -1
  30. package/dist/config/contracts.js.map +0 -1
  31. package/dist/config/index.d.ts.map +0 -1
  32. package/dist/config/index.js.map +0 -1
  33. package/dist/config/networks.d.ts.map +0 -1
  34. package/dist/config/networks.js.map +0 -1
  35. package/dist/config/pillar.d.ts.map +0 -1
  36. package/dist/config/pillar.js.map +0 -1
  37. package/dist/config/sponsor.d.ts.map +0 -1
  38. package/dist/config/sponsor.js.map +0 -1
  39. package/dist/endpoints/index.d.ts.map +0 -1
  40. package/dist/endpoints/index.js.map +0 -1
  41. package/dist/endpoints/registry.d.ts.map +0 -1
  42. package/dist/endpoints/registry.js.map +0 -1
  43. package/dist/index.d.ts.map +0 -1
  44. package/dist/index.js.map +0 -1
  45. package/dist/services/bitflow.service.d.ts.map +0 -1
  46. package/dist/services/bitflow.service.js.map +0 -1
  47. package/dist/services/bns.service.d.ts.map +0 -1
  48. package/dist/services/bns.service.js.map +0 -1
  49. package/dist/services/credentials.d.ts.map +0 -1
  50. package/dist/services/credentials.js.map +0 -1
  51. package/dist/services/defi.service.d.ts.map +0 -1
  52. package/dist/services/defi.service.js.map +0 -1
  53. package/dist/services/erc8004.service.d.ts.map +0 -1
  54. package/dist/services/erc8004.service.js.map +0 -1
  55. package/dist/services/hiro-api.d.ts.map +0 -1
  56. package/dist/services/hiro-api.js.map +0 -1
  57. package/dist/services/inscription-parser.d.ts.map +0 -1
  58. package/dist/services/inscription-parser.js.map +0 -1
  59. package/dist/services/lightning/provider.d.ts.map +0 -1
  60. package/dist/services/lightning/provider.js.map +0 -1
  61. package/dist/services/lightning/spark-provider.d.ts.map +0 -1
  62. package/dist/services/lightning/spark-provider.js.map +0 -1
  63. package/dist/services/lightning-manager.d.ts.map +0 -1
  64. package/dist/services/lightning-manager.js.map +0 -1
  65. package/dist/services/mempool-api.d.ts.map +0 -1
  66. package/dist/services/mempool-api.js.map +0 -1
  67. package/dist/services/nft.service.d.ts.map +0 -1
  68. package/dist/services/nft.service.js.map +0 -1
  69. package/dist/services/nonce-tracker.d.ts.map +0 -1
  70. package/dist/services/nonce-tracker.js.map +0 -1
  71. package/dist/services/pillar-api.service.d.ts.map +0 -1
  72. package/dist/services/pillar-api.service.js.map +0 -1
  73. package/dist/services/sbtc-deposit.service.d.ts.map +0 -1
  74. package/dist/services/sbtc-deposit.service.js.map +0 -1
  75. package/dist/services/sbtc.service.d.ts.map +0 -1
  76. package/dist/services/sbtc.service.js.map +0 -1
  77. package/dist/services/scaffold.service.d.ts.map +0 -1
  78. package/dist/services/scaffold.service.js.map +0 -1
  79. package/dist/services/signing-key.service.d.ts.map +0 -1
  80. package/dist/services/signing-key.service.js.map +0 -1
  81. package/dist/services/stacking.service.d.ts.map +0 -1
  82. package/dist/services/stacking.service.js.map +0 -1
  83. package/dist/services/tenero-api.d.ts.map +0 -1
  84. package/dist/services/tenero-api.js.map +0 -1
  85. package/dist/services/tokens.service.d.ts.map +0 -1
  86. package/dist/services/tokens.service.js.map +0 -1
  87. package/dist/services/unisat-indexer.d.ts.map +0 -1
  88. package/dist/services/unisat-indexer.js.map +0 -1
  89. package/dist/services/wallet-manager.d.ts.map +0 -1
  90. package/dist/services/wallet-manager.js.map +0 -1
  91. package/dist/services/x402.service.d.ts.map +0 -1
  92. package/dist/services/x402.service.js.map +0 -1
  93. package/dist/tools/arxiv-research.tools.d.ts.map +0 -1
  94. package/dist/tools/arxiv-research.tools.js.map +0 -1
  95. package/dist/tools/bitcoin.tools.d.ts.map +0 -1
  96. package/dist/tools/bitcoin.tools.js.map +0 -1
  97. package/dist/tools/bitflow.tools.d.ts.map +0 -1
  98. package/dist/tools/bitflow.tools.js.map +0 -1
  99. package/dist/tools/bns.tools.d.ts.map +0 -1
  100. package/dist/tools/bns.tools.js.map +0 -1
  101. package/dist/tools/bounty-scanner.tools.d.ts.map +0 -1
  102. package/dist/tools/bounty-scanner.tools.js.map +0 -1
  103. package/dist/tools/child-inscription.tools.d.ts.map +0 -1
  104. package/dist/tools/child-inscription.tools.js.map +0 -1
  105. package/dist/tools/competition.tools.d.ts.map +0 -1
  106. package/dist/tools/competition.tools.js.map +0 -1
  107. package/dist/tools/contract.tools.d.ts.map +0 -1
  108. package/dist/tools/contract.tools.js.map +0 -1
  109. package/dist/tools/credentials.tools.d.ts.map +0 -1
  110. package/dist/tools/credentials.tools.js.map +0 -1
  111. package/dist/tools/defi.tools.d.ts.map +0 -1
  112. package/dist/tools/defi.tools.js.map +0 -1
  113. package/dist/tools/dual-stacking.tools.d.ts.map +0 -1
  114. package/dist/tools/dual-stacking.tools.js.map +0 -1
  115. package/dist/tools/earning.tools.d.ts.map +0 -1
  116. package/dist/tools/earning.tools.js.map +0 -1
  117. package/dist/tools/endpoint.tools.d.ts.map +0 -1
  118. package/dist/tools/endpoint.tools.js.map +0 -1
  119. package/dist/tools/erc8004.tools.d.ts.map +0 -1
  120. package/dist/tools/erc8004.tools.js.map +0 -1
  121. package/dist/tools/identity.tools.d.ts.map +0 -1
  122. package/dist/tools/identity.tools.js.map +0 -1
  123. package/dist/tools/inbox-x402.tools.d.ts.map +0 -1
  124. package/dist/tools/inbox-x402.tools.js.map +0 -1
  125. package/dist/tools/inbox.tools.d.ts.map +0 -1
  126. package/dist/tools/inbox.tools.js.map +0 -1
  127. package/dist/tools/index.d.ts.map +0 -1
  128. package/dist/tools/index.js.map +0 -1
  129. package/dist/tools/jingswap.tools.d.ts.map +0 -1
  130. package/dist/tools/jingswap.tools.js.map +0 -1
  131. package/dist/tools/lightning.tools.d.ts.map +0 -1
  132. package/dist/tools/lightning.tools.js.map +0 -1
  133. package/dist/tools/mempool.tools.d.ts.map +0 -1
  134. package/dist/tools/mempool.tools.js.map +0 -1
  135. package/dist/tools/news.tools.d.ts.map +0 -1
  136. package/dist/tools/news.tools.js.map +0 -1
  137. package/dist/tools/nft.tools.d.ts.map +0 -1
  138. package/dist/tools/nft.tools.js.map +0 -1
  139. package/dist/tools/nonce.tools.d.ts.map +0 -1
  140. package/dist/tools/nonce.tools.js.map +0 -1
  141. package/dist/tools/nostr.tools.d.ts.map +0 -1
  142. package/dist/tools/nostr.tools.js.map +0 -1
  143. package/dist/tools/openrouter.tools.d.ts.map +0 -1
  144. package/dist/tools/openrouter.tools.js.map +0 -1
  145. package/dist/tools/ordinals-marketplace.tools.d.ts.map +0 -1
  146. package/dist/tools/ordinals-marketplace.tools.js.map +0 -1
  147. package/dist/tools/ordinals-p2p.tools.d.ts.map +0 -1
  148. package/dist/tools/ordinals-p2p.tools.js.map +0 -1
  149. package/dist/tools/ordinals.tools.d.ts.map +0 -1
  150. package/dist/tools/ordinals.tools.js.map +0 -1
  151. package/dist/tools/pillar-direct.tools.d.ts.map +0 -1
  152. package/dist/tools/pillar-direct.tools.js.map +0 -1
  153. package/dist/tools/pillar.tools.d.ts.map +0 -1
  154. package/dist/tools/pillar.tools.js.map +0 -1
  155. package/dist/tools/psbt.helpers.d.ts.map +0 -1
  156. package/dist/tools/psbt.helpers.js.map +0 -1
  157. package/dist/tools/psbt.tools.d.ts.map +0 -1
  158. package/dist/tools/psbt.tools.js.map +0 -1
  159. package/dist/tools/query.tools.d.ts.map +0 -1
  160. package/dist/tools/query.tools.js.map +0 -1
  161. package/dist/tools/relay-diagnostic.tools.d.ts.map +0 -1
  162. package/dist/tools/relay-diagnostic.tools.js.map +0 -1
  163. package/dist/tools/reputation.tools.d.ts.map +0 -1
  164. package/dist/tools/reputation.tools.js.map +0 -1
  165. package/dist/tools/runes.tools.d.ts.map +0 -1
  166. package/dist/tools/runes.tools.js.map +0 -1
  167. package/dist/tools/sbtc.tools.d.ts.map +0 -1
  168. package/dist/tools/sbtc.tools.js.map +0 -1
  169. package/dist/tools/scaffold.tools.d.ts.map +0 -1
  170. package/dist/tools/scaffold.tools.js.map +0 -1
  171. package/dist/tools/schemas.d.ts.map +0 -1
  172. package/dist/tools/schemas.js.map +0 -1
  173. package/dist/tools/settings.tools.d.ts.map +0 -1
  174. package/dist/tools/settings.tools.js.map +0 -1
  175. package/dist/tools/signing.tools.d.ts.map +0 -1
  176. package/dist/tools/signing.tools.js.map +0 -1
  177. package/dist/tools/skill-mappings.d.ts.map +0 -1
  178. package/dist/tools/skill-mappings.js.map +0 -1
  179. package/dist/tools/souldinals.tools.d.ts.map +0 -1
  180. package/dist/tools/souldinals.tools.js.map +0 -1
  181. package/dist/tools/stacking-lottery.tools.d.ts.map +0 -1
  182. package/dist/tools/stacking-lottery.tools.js.map +0 -1
  183. package/dist/tools/stacking.tools.d.ts.map +0 -1
  184. package/dist/tools/stacking.tools.js.map +0 -1
  185. package/dist/tools/stacks-market.tools.d.ts.map +0 -1
  186. package/dist/tools/stacks-market.tools.js.map +0 -1
  187. package/dist/tools/styx.tools.d.ts.map +0 -1
  188. package/dist/tools/styx.tools.js.map +0 -1
  189. package/dist/tools/taproot-multisig.tools.d.ts.map +0 -1
  190. package/dist/tools/taproot-multisig.tools.js.map +0 -1
  191. package/dist/tools/tenero.tools.d.ts.map +0 -1
  192. package/dist/tools/tenero.tools.js.map +0 -1
  193. package/dist/tools/tokens.tools.d.ts.map +0 -1
  194. package/dist/tools/tokens.tools.js.map +0 -1
  195. package/dist/tools/transfer.tools.d.ts.map +0 -1
  196. package/dist/tools/transfer.tools.js.map +0 -1
  197. package/dist/tools/wallet-management.tools.d.ts.map +0 -1
  198. package/dist/tools/wallet-management.tools.js.map +0 -1
  199. package/dist/tools/wallet.tools.d.ts.map +0 -1
  200. package/dist/tools/wallet.tools.js.map +0 -1
  201. package/dist/tools/yield-dashboard.tools.d.ts.map +0 -1
  202. package/dist/tools/yield-dashboard.tools.js.map +0 -1
  203. package/dist/tools/yield-hunter.tools.d.ts.map +0 -1
  204. package/dist/tools/yield-hunter.tools.js.map +0 -1
  205. package/dist/transactions/bitcoin-builder.d.ts.map +0 -1
  206. package/dist/transactions/bitcoin-builder.js.map +0 -1
  207. package/dist/transactions/builder.d.ts.map +0 -1
  208. package/dist/transactions/builder.js.map +0 -1
  209. package/dist/transactions/child-inscription-builder.d.ts.map +0 -1
  210. package/dist/transactions/child-inscription-builder.js.map +0 -1
  211. package/dist/transactions/clarity-values.d.ts.map +0 -1
  212. package/dist/transactions/clarity-values.js.map +0 -1
  213. package/dist/transactions/inscription-builder.d.ts.map +0 -1
  214. package/dist/transactions/inscription-builder.js.map +0 -1
  215. package/dist/transactions/post-conditions.d.ts.map +0 -1
  216. package/dist/transactions/post-conditions.js.map +0 -1
  217. package/dist/transactions/rune-transfer-builder.d.ts.map +0 -1
  218. package/dist/transactions/rune-transfer-builder.js.map +0 -1
  219. package/dist/transactions/runestone-builder.d.ts.map +0 -1
  220. package/dist/transactions/runestone-builder.js.map +0 -1
  221. package/dist/transactions/sponsor-builder.d.ts.map +0 -1
  222. package/dist/transactions/sponsor-builder.js.map +0 -1
  223. package/dist/utils/bip322.d.ts.map +0 -1
  224. package/dist/utils/bip322.js.map +0 -1
  225. package/dist/utils/bitcoin.d.ts.map +0 -1
  226. package/dist/utils/bitcoin.js.map +0 -1
  227. package/dist/utils/encryption.d.ts.map +0 -1
  228. package/dist/utils/encryption.js.map +0 -1
  229. package/dist/utils/erc8004-helpers.d.ts.map +0 -1
  230. package/dist/utils/erc8004-helpers.js.map +0 -1
  231. package/dist/utils/errors.d.ts.map +0 -1
  232. package/dist/utils/errors.js.map +0 -1
  233. package/dist/utils/fee.d.ts.map +0 -1
  234. package/dist/utils/fee.js.map +0 -1
  235. package/dist/utils/formatting.d.ts.map +0 -1
  236. package/dist/utils/formatting.js.map +0 -1
  237. package/dist/utils/index.d.ts.map +0 -1
  238. package/dist/utils/index.js.map +0 -1
  239. package/dist/utils/l402-protocol.d.ts.map +0 -1
  240. package/dist/utils/l402-protocol.js.map +0 -1
  241. package/dist/utils/redact.d.ts.map +0 -1
  242. package/dist/utils/redact.js.map +0 -1
  243. package/dist/utils/relay-health.d.ts.map +0 -1
  244. package/dist/utils/relay-health.js.map +0 -1
  245. package/dist/utils/storage.d.ts.map +0 -1
  246. package/dist/utils/storage.js.map +0 -1
  247. package/dist/utils/tokens.d.ts.map +0 -1
  248. package/dist/utils/tokens.js.map +0 -1
  249. package/dist/utils/x402-payment-logging.d.ts.map +0 -1
  250. package/dist/utils/x402-payment-logging.js.map +0 -1
  251. package/dist/utils/x402-payment-state.d.ts.map +0 -1
  252. package/dist/utils/x402-payment-state.js.map +0 -1
  253. package/dist/utils/x402-protocol.d.ts.map +0 -1
  254. package/dist/utils/x402-protocol.js.map +0 -1
  255. package/dist/utils/x402-recovery.d.ts.map +0 -1
  256. package/dist/utils/x402-recovery.js.map +0 -1
  257. package/dist/yield-hunter/index.d.ts.map +0 -1
  258. package/dist/yield-hunter/index.js.map +0 -1
package/dist/index.js CHANGED
@@ -36,13 +36,24 @@ function getClaudeDesktopConfigPath() {
36
36
  }
37
37
  }
38
38
  async function readJsonConfig(filePath) {
39
+ let content;
40
+ try {
41
+ content = await fs.readFile(filePath, "utf8");
42
+ }
43
+ catch (error) {
44
+ if (error.code === "ENOENT") {
45
+ return {}; // No config yet - fresh install
46
+ }
47
+ throw error;
48
+ }
39
49
  try {
40
- const content = await fs.readFile(filePath, "utf8");
41
50
  return JSON.parse(content);
42
51
  }
43
52
  catch {
44
- // File doesn't exist or isn't valid JSON, start fresh
45
- return {};
53
+ // Never silently replace an unparseable config: it may hold the user's
54
+ // other MCP servers, and "merging" into {} would discard them on write.
55
+ throw new Error(`Existing config at ${filePath} is not valid JSON. ` +
56
+ `Fix or remove it, then re-run the install.`);
46
57
  }
47
58
  }
48
59
  async function writeJsonConfig(filePath, config) {
@@ -99,8 +110,10 @@ async function writeCodexToml(configPath, network) {
99
110
  try {
100
111
  existing = await fs.readFile(configPath, "utf8");
101
112
  }
102
- catch {
103
- existing = "";
113
+ catch (error) {
114
+ if (error.code !== "ENOENT") {
115
+ throw error; // Unreadable existing config must not be clobbered
116
+ }
104
117
  }
105
118
  const preserved = stripCodexSection(existing).replace(/\s+$/, "");
106
119
  const block = [
@@ -39,6 +39,7 @@ async function encrypt(data, password) {
39
39
  const iv = crypto.randomBytes(12);
40
40
  const key = await deriveKey(password, salt);
41
41
  const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
42
+ key.fill(0); // cipher holds its own copy
42
43
  const encrypted = Buffer.concat([cipher.update(data, "utf8"), cipher.final()]);
43
44
  const authTag = cipher.getAuthTag();
44
45
  return {
@@ -53,6 +54,7 @@ async function encrypt(data, password) {
53
54
  async function decrypt(encrypted, password) {
54
55
  const key = await deriveKey(password, Buffer.from(encrypted.salt, "base64"));
55
56
  const decipher = crypto.createDecipheriv("aes-256-gcm", key, Buffer.from(encrypted.iv, "base64"));
57
+ key.fill(0); // decipher holds its own copy
56
58
  decipher.setAuthTag(Buffer.from(encrypted.authTag, "base64"));
57
59
  const decrypted = Buffer.concat([
58
60
  decipher.update(Buffer.from(encrypted.ciphertext, "base64")),
@@ -67,12 +69,15 @@ function emptyStore() {
67
69
  async function save() {
68
70
  if (!_store || !_password)
69
71
  throw new Error("Store not unlocked");
70
- await fs.mkdir(getStoreDir(), { recursive: true });
72
+ await fs.mkdir(getStoreDir(), { recursive: true, mode: 0o700 });
71
73
  const file = {
72
74
  version: VERSION,
73
75
  encrypted: await encrypt(JSON.stringify(_store), _password),
74
76
  };
75
- await fs.writeFile(getStoreFile(), JSON.stringify(file, null, 2));
77
+ // Owner-only perms + atomic rename, same pattern as the wallet keystores
78
+ const tempFile = getStoreFile() + ".tmp";
79
+ await fs.writeFile(tempFile, JSON.stringify(file, null, 2), { mode: 0o600 });
80
+ await fs.rename(tempFile, getStoreFile());
76
81
  }
77
82
  async function load(password) {
78
83
  const raw = await fs.readFile(getStoreFile(), "utf-8");
@@ -1,6 +1,6 @@
1
1
  import { uintCV, contractPrincipalCV, cvToJSON, hexToCV, PostConditionMode, Pc, principalCV, noneCV, someCV, listCV, bufferCV, } from "@stacks/transactions";
2
2
  import { AlexSDK, Currency } from "alex-sdk";
3
- import { getHiroApi } from "./hiro-api.js";
3
+ import { getHiroApi, isNotFoundError } from "./hiro-api.js";
4
4
  import { getAlexContracts, getZestContracts, parseContractId, ZEST_ASSETS, ZEST_V2_MARKET, ZEST_V2_MARKET_VAULT, } from "../config/index.js";
5
5
  import { callContract } from "../transactions/builder.js";
6
6
  // ============================================================================
@@ -129,8 +129,10 @@ export class AlexDexService {
129
129
  }
130
130
  return null;
131
131
  }
132
- catch {
133
- return null;
132
+ catch (error) {
133
+ if (isNotFoundError(error))
134
+ return null;
135
+ throw error;
134
136
  }
135
137
  }
136
138
  /**
@@ -360,8 +362,10 @@ export class ZestProtocolService {
360
362
  healthFactor: position["health-factor"]?.value,
361
363
  };
362
364
  }
363
- catch {
364
- return null;
365
+ catch (error) {
366
+ if (isNotFoundError(error))
367
+ return null;
368
+ throw error;
365
369
  }
366
370
  }
367
371
  /**
@@ -377,8 +381,10 @@ export class ZestProtocolService {
377
381
  }
378
382
  return cvToJSON(hexToCV(result.result));
379
383
  }
380
- catch {
381
- return null;
384
+ catch (error) {
385
+ if (isNotFoundError(error))
386
+ return null;
387
+ throw error;
382
388
  }
383
389
  }
384
390
  /**
@@ -411,8 +417,12 @@ export class ZestProtocolService {
411
417
  }
412
418
  }
413
419
  }
414
- catch {
415
- // Fall back to shares as lower bound estimate
420
+ catch (error) {
421
+ // Only fall back to the shares lower-bound estimate when the vault
422
+ // function is missing — network/server failures must surface, or the
423
+ // post-conditions built from this value would be silently wrong.
424
+ if (!isNotFoundError(error))
425
+ throw error;
416
426
  }
417
427
  return shares;
418
428
  }
@@ -431,8 +441,10 @@ export class ZestProtocolService {
431
441
  }
432
442
  }
433
443
  }
434
- catch {
435
- // Fall back to amount as upper bound
444
+ catch (error) {
445
+ // Same rationale as getUnderlyingFromShares: estimate only on 404.
446
+ if (!isNotFoundError(error))
447
+ throw error;
436
448
  }
437
449
  return amount;
438
450
  }
@@ -7,6 +7,16 @@ export declare class HiroApiRateLimitError extends Error {
7
7
  readonly retryAfterSeconds: number;
8
8
  constructor(message: string, retryAfterSeconds: number);
9
9
  }
10
+ /**
11
+ * HTTP error from the Hiro / BNS V2 APIs, carrying the status code so
12
+ * callers can treat 404 as "not found" without swallowing other failures.
13
+ */
14
+ export declare class HiroApiError extends Error {
15
+ readonly status: number;
16
+ constructor(message: string, status: number);
17
+ }
18
+ /** True when the error is an expected "resource does not exist" response. */
19
+ export declare function isNotFoundError(error: unknown): boolean;
10
20
  export interface AccountInfo {
11
21
  balance: string;
12
22
  locked: string;
@@ -16,6 +16,22 @@ export class HiroApiRateLimitError extends Error {
16
16
  this.name = "HiroApiRateLimitError";
17
17
  }
18
18
  }
19
+ /**
20
+ * HTTP error from the Hiro / BNS V2 APIs, carrying the status code so
21
+ * callers can treat 404 as "not found" without swallowing other failures.
22
+ */
23
+ export class HiroApiError extends Error {
24
+ status;
25
+ constructor(message, status) {
26
+ super(message);
27
+ this.status = status;
28
+ this.name = "HiroApiError";
29
+ }
30
+ }
31
+ /** True when the error is an expected "resource does not exist" response. */
32
+ export function isNotFoundError(error) {
33
+ return error instanceof HiroApiError && error.status === 404;
34
+ }
19
35
  // ============================================================================
20
36
  // Hiro API Service
21
37
  // ============================================================================
@@ -51,7 +67,7 @@ export class HiroApiService {
51
67
  throw new HiroApiRateLimitError(`Hiro API rate limit exceeded. Retry after ${retryAfterSeconds}s`, retryAfterSeconds);
52
68
  }
53
69
  const errorText = await response.text();
54
- throw new Error(`Hiro API error (${response.status}): ${errorText}`);
70
+ throw new HiroApiError(`Hiro API error (${response.status}): ${errorText}`, response.status);
55
71
  }
56
72
  return response.json();
57
73
  }
@@ -123,8 +139,10 @@ export class HiroApiService {
123
139
  try {
124
140
  return await this.fetch(`/metadata/v1/ft/${contractId}`);
125
141
  }
126
- catch {
127
- return null;
142
+ catch (error) {
143
+ if (isNotFoundError(error))
144
+ return null;
145
+ throw error;
128
146
  }
129
147
  }
130
148
  async getTokenHolders(contractId, options) {
@@ -329,8 +347,10 @@ export class HiroApiService {
329
347
  const info = await this.getBnsNameInfo(name);
330
348
  return info.address;
331
349
  }
332
- catch {
333
- return null;
350
+ catch (error) {
351
+ if (isNotFoundError(error))
352
+ return null;
353
+ throw error;
334
354
  }
335
355
  }
336
356
  // ==========================================================================
@@ -385,7 +405,7 @@ export class BnsV2ApiService {
385
405
  });
386
406
  if (!response.ok) {
387
407
  const errorText = await response.text();
388
- throw new Error(`BNS V2 API error (${response.status}): ${errorText}`);
408
+ throw new HiroApiError(`BNS V2 API error (${response.status}): ${errorText}`, response.status);
389
409
  }
390
410
  return response.json();
391
411
  }
@@ -431,8 +451,10 @@ export class BnsV2ApiService {
431
451
  }
432
452
  return null;
433
453
  }
434
- catch {
435
- return null;
454
+ catch (error) {
455
+ if (isNotFoundError(error))
456
+ return null;
457
+ throw error;
436
458
  }
437
459
  }
438
460
  }
@@ -1,5 +1,5 @@
1
1
  import { uintCV, principalCV } from "@stacks/transactions";
2
- import { getHiroApi } from "./hiro-api.js";
2
+ import { getHiroApi, HiroApiError } from "./hiro-api.js";
3
3
  import { parseContractId } from "../config/index.js";
4
4
  import { callContract } from "../transactions/builder.js";
5
5
  import { createNftSendPostCondition } from "../transactions/post-conditions.js";
@@ -51,8 +51,13 @@ export class NftService {
51
51
  }
52
52
  return null;
53
53
  }
54
- catch {
55
- return null;
54
+ catch (error) {
55
+ // 4xx means the contract doesn't implement get-owner (or rejects the
56
+ // call) — that is "no owner". Network/server failures must surface.
57
+ if (error instanceof HiroApiError && error.status < 500) {
58
+ return null;
59
+ }
60
+ throw error;
56
61
  }
57
62
  }
58
63
  /**
@@ -75,8 +80,12 @@ export class NftService {
75
80
  }
76
81
  }
77
82
  }
78
- catch {
79
- // Function may not exist or may fail
83
+ catch (error) {
84
+ // 4xx means the contract doesn't implement get-last-token-id —
85
+ // supply is simply unknown. Network/server failures must surface.
86
+ if (!(error instanceof HiroApiError && error.status < 500)) {
87
+ throw error;
88
+ }
80
89
  }
81
90
  return {
82
91
  contractId,
@@ -1,3 +1,11 @@
1
+ /**
2
+ * HTTP error from the Pillar API, carrying the status code so callers can
3
+ * treat 4xx ("no such wallet / no position") differently from infra failures.
4
+ */
5
+ export declare class PillarApiError extends Error {
6
+ readonly status: number;
7
+ constructor(message: string, status: number);
8
+ }
1
9
  declare class PillarApiService {
2
10
  private baseUrl;
3
11
  private apiKey;
@@ -1,4 +1,16 @@
1
1
  import { PILLAR_API_URL, PILLAR_API_KEY } from "../config/pillar.js";
2
+ /**
3
+ * HTTP error from the Pillar API, carrying the status code so callers can
4
+ * treat 4xx ("no such wallet / no position") differently from infra failures.
5
+ */
6
+ export class PillarApiError extends Error {
7
+ status;
8
+ constructor(message, status) {
9
+ super(message);
10
+ this.status = status;
11
+ this.name = "PillarApiError";
12
+ }
13
+ }
2
14
  class PillarApiService {
3
15
  baseUrl;
4
16
  apiKey;
@@ -32,7 +44,7 @@ class PillarApiService {
32
44
  });
33
45
  if (!response.ok) {
34
46
  const errorText = await response.text();
35
- throw new Error(`Pillar API error (${response.status}): ${errorText}`);
47
+ throw new PillarApiError(`Pillar API error (${response.status}): ${errorText}`, response.status);
36
48
  }
37
49
  return response.json();
38
50
  }
@@ -1,5 +1,5 @@
1
1
  import axios from "axios";
2
- import { makeSTXTokenTransfer, makeContractCall, uintCV, principalCV, noneCV, PostConditionMode, } from "@stacks/transactions";
2
+ import { makeSTXTokenTransfer, makeContractCall, uintCV, principalCV, noneCV, Pc, PostConditionMode, } from "@stacks/transactions";
3
3
  import { decodePaymentRequired, encodePaymentPayload, generatePaymentId, buildPaymentIdentifierExtension, X402_HEADERS, } from "../utils/x402-protocol.js";
4
4
  import { generateWallet, getStxAddress } from "@stacks/wallet-sdk";
5
5
  import { NETWORK, API_URL, getStacksNetwork } from "../config/networks.js";
@@ -11,6 +11,7 @@ import { getHiroApi } from "./hiro-api.js";
11
11
  import { createHash } from "crypto";
12
12
  import { InsufficientBalanceError } from "../utils/errors.js";
13
13
  import { getContracts, parseContractId } from "../config/contracts.js";
14
+ import { resolveDefaultFee } from "../utils/fee.js";
14
15
  import { classifyCanonicalPaymentStatus, formatCanonicalPaymentStatus, resolveCanonicalPaymentStatus, } from "../utils/x402-payment-state.js";
15
16
  import { extractPaymentIdFromPaymentSignature } from "../utils/x402-recovery.js";
16
17
  import { emitCanonicalPaymentDecisionLogs, emitCanonicalPaymentPollLogs, emitPaymentLog, } from "../utils/x402-payment-logging.js";
@@ -42,6 +43,16 @@ function parseSatsCap(envName, fallback) {
42
43
  return parsed;
43
44
  }
44
45
  const L402_MAX_SATS_PER_INVOICE = parseSatsCap("L402_MAX_SATS_PER_INVOICE", DEFAULT_L402_MAX_SATS);
46
+ /**
47
+ * Per-payment caps for the Stacks x402 interceptor, mirroring the L402 cap
48
+ * above: a malicious endpoint can demand an arbitrary amount in its 402
49
+ * response and an autoApprove'd call would pay it, bounded only by wallet
50
+ * balance. Known registry endpoints cost at most 0.02 STX / 100 sats, so the
51
+ * defaults leave generous headroom. Overridable via env vars.
52
+ */
53
+ const X402_MAX_USTX_PER_PAYMENT = parseSatsCap("X402_MAX_USTX_PER_PAYMENT", 1_000_000 // 1 STX
54
+ );
55
+ const X402_MAX_SATS_PER_PAYMENT = parseSatsCap("X402_MAX_SATS_PER_PAYMENT", 10_000);
45
56
  // Track payment attempts per client instance (auto-cleanup via WeakMap)
46
57
  const paymentAttempts = new WeakMap();
47
58
  // Track 429 retry counts per client instance
@@ -457,6 +468,16 @@ export async function createApiClient(baseUrl, options) {
457
468
  const networks = paymentRequired.accepts.map((a) => a.network).join(", ");
458
469
  return Promise.reject(new Error(`No compatible Stacks payment option found. Available networks: ${networks}`));
459
470
  }
471
+ const tokenType = detectTokenType(selectedOption.asset);
472
+ const amount = BigInt(selectedOption.amount);
473
+ const isSbtc = tokenType === "sBTC";
474
+ const cap = isSbtc ? X402_MAX_SATS_PER_PAYMENT : X402_MAX_USTX_PER_PAYMENT;
475
+ const unit = isSbtc ? "sats" : "uSTX";
476
+ if (amount > BigInt(Math.floor(cap))) {
477
+ return Promise.reject(new Error(`x402 payment of ${amount} ${unit} exceeds the per-payment cap of ${cap} ${unit}. ` +
478
+ `If this cost is expected, raise the cap via the ` +
479
+ `${isSbtc ? "X402_MAX_SATS_PER_PAYMENT" : "X402_MAX_USTX_PER_PAYMENT"} env var.`));
480
+ }
460
481
  // Lazy-load account on first 402 — free endpoints never reach here
461
482
  const acct = await ensureAccount();
462
483
  // Verify the payment network matches our configured network
@@ -476,9 +497,11 @@ export async function createApiClient(baseUrl, options) {
476
497
  account: acct,
477
498
  });
478
499
  }
479
- // Build a sponsored signed transaction (relay pays gas; fee: 0n)
480
- const tokenType = detectTokenType(selectedOption.asset);
481
- const amount = BigInt(selectedOption.amount);
500
+ // Build a non-sponsored signed transaction: the sender pays its own
501
+ // STX gas (fee + nonce auto-estimated by @stacks), and the x402
502
+ // facilitator simply verifies and broadcasts it. No relay sponsorship
503
+ // — this is the same tx shape the x402-stacks lib produces, and the
504
+ // balance check in onBeforePayment already accounts for the gas.
482
505
  const networkName = getStacksNetwork(acct.network);
483
506
  let transaction;
484
507
  if (tokenType === "sBTC") {
@@ -496,9 +519,18 @@ export async function createApiClient(baseUrl, options) {
496
519
  ],
497
520
  senderKey: acct.privateKey,
498
521
  network: networkName,
499
- postConditionMode: PostConditionMode.Allow,
500
- sponsored: true,
501
- fee: 0n,
522
+ // The payment amount is known at build time, so pin it: a
523
+ // compromised token contract must not be able to pull more.
524
+ postConditionMode: PostConditionMode.Deny,
525
+ postConditions: [
526
+ Pc.principal(acct.address)
527
+ .willSendEq(amount)
528
+ .ft(contracts.SBTC_TOKEN, "sbtc-token"),
529
+ ],
530
+ // Clamped medium fee — do NOT let @stacks auto-estimate, since
531
+ // Hiro's contract_call high_priority tier is polluted by outliers
532
+ // (observed >2000 STX). resolveDefaultFee caps it at 0.05 STX.
533
+ fee: await resolveDefaultFee(acct.network, "contract_call"),
502
534
  });
503
535
  }
504
536
  else {
@@ -508,8 +540,7 @@ export async function createApiClient(baseUrl, options) {
508
540
  senderKey: acct.privateKey,
509
541
  network: networkName,
510
542
  memo: "",
511
- sponsored: true,
512
- fee: 0n,
543
+ fee: await resolveDefaultFee(acct.network, "token_transfer"),
513
544
  });
514
545
  }
515
546
  const txHex = "0x" + transaction.serialize();
@@ -716,8 +747,9 @@ export async function checkSufficientBalance(account, amount, asset, sponsored =
716
747
  const hiroApiForSbtc = getHiroApi(account.network);
717
748
  const stxInfoForSbtc = await hiroApiForSbtc.getStxBalance(account.address);
718
749
  const stxBalanceForSbtc = BigInt(stxInfoForSbtc.balance);
719
- const sbtcFees = await hiroApiForSbtc.getMempoolFees();
720
- const estimatedSbtcFee = BigInt(sbtcFees.contract_call.high_priority);
750
+ // Same clamped fee the interceptor actually sets on the sBTC contract
751
+ // call — so this check is exact, not a high-priority over-estimate.
752
+ const estimatedSbtcFee = await resolveDefaultFee(account.network, "contract_call");
721
753
  if (stxBalanceForSbtc < estimatedSbtcFee) {
722
754
  const stxShortfall = estimatedSbtcFee - stxBalanceForSbtc;
723
755
  throw new InsufficientBalanceError(`Insufficient STX balance to cover sBTC transfer fee: need ${formatStx(estimatedSbtcFee.toString())} estimated fee, ` +
@@ -733,8 +765,8 @@ export async function checkSufficientBalance(account, amount, asset, sponsored =
733
765
  const balance = BigInt(balanceInfo.balance);
734
766
  let totalRequired = requiredAmount;
735
767
  if (!sponsored) {
736
- const mempoolFees = await hiroApi.getMempoolFees();
737
- const estimatedFee = BigInt(mempoolFees.contract_call.high_priority);
768
+ // Same clamped fee the interceptor sets on the STX transfer.
769
+ const estimatedFee = await resolveDefaultFee(account.network, "token_transfer");
738
770
  totalRequired = requiredAmount + estimatedFee;
739
771
  }
740
772
  if (balance >= totalRequired)
@@ -33,6 +33,19 @@ async function getBtcAddress(providedAddress) {
33
33
  throw new Error("No Bitcoin address provided and wallet is not unlocked. " +
34
34
  "Either provide an address or unlock your wallet first.");
35
35
  }
36
+ /**
37
+ * Reject recipient addresses that belong to the other Bitcoin network before
38
+ * any UTXO fetching. The tx builder (@scure/btc-signer) rejects these too, but
39
+ * late and with a cryptic bech32/version-byte error.
40
+ */
41
+ function validateRecipientNetwork(address) {
42
+ const prefixes = NETWORK === "mainnet" ? ["bc1", "1", "3"] : ["tb1", "m", "n", "2"];
43
+ if (!prefixes.some((p) => address.startsWith(p))) {
44
+ throw new Error(`Recipient "${address}" is not a ${NETWORK} address. ` +
45
+ `Expected a prefix of ${prefixes.join(", ")} — check that the address ` +
46
+ `matches the configured NETWORK.`);
47
+ }
48
+ }
36
49
  /**
37
50
  * Format satoshis as BTC string
38
51
  */
@@ -231,6 +244,7 @@ export function registerBitcoinTools(server) {
231
244
  },
232
245
  }, async ({ recipient, amount, feeRate, includeOrdinals }) => {
233
246
  try {
247
+ validateRecipientNetwork(recipient);
234
248
  // Get wallet account (requires unlocked wallet)
235
249
  const walletManager = getWalletManager();
236
250
  const account = walletManager.getActiveAccount();
@@ -276,7 +276,9 @@ For aibtc.com inbox messages, use send_inbox_message_direct instead — it signs
276
276
  const api = await createApiClient(parsed.baseUrl, {
277
277
  toolName: "execute_x402_endpoint",
278
278
  onBeforePayment: async (requirements) => {
279
- await checkSufficientBalance(requirements.account, requirements.amount, requirements.asset, true);
279
+ // Non-sponsored: sender pays its own gas, so validate STX for the
280
+ // fee too (sponsored=false is the default, passed explicitly here).
281
+ await checkSufficientBalance(requirements.account, requirements.amount, requirements.asset, false);
280
282
  },
281
283
  });
282
284
  const response = await api.request({ method, url: parsed.requestPath, params, data });
@@ -40,21 +40,18 @@ async function getNextNonce(address) {
40
40
  const hiroApi = getHiroApi(NETWORK);
41
41
  const accountInfo = await hiroApi.getAccountInfo(address);
42
42
  const confirmedNonce = accountInfo.nonce;
43
+ // The mempool scan is what makes the nonce safe against pending txs —
44
+ // silently skipping it on failure risks a nonce collision, so let it fail.
43
45
  let highestMempoolNonce = -1;
44
- try {
45
- const mempool = await hiroApi.getMempoolTransactions({
46
- sender_address: address,
47
- limit: 50,
48
- });
49
- for (const tx of mempool.results) {
50
- if (tx.nonce > highestMempoolNonce) {
51
- highestMempoolNonce = tx.nonce;
52
- }
46
+ const mempool = await hiroApi.getMempoolTransactions({
47
+ sender_address: address,
48
+ limit: 50,
49
+ });
50
+ for (const tx of mempool.results) {
51
+ if (tx.nonce > highestMempoolNonce) {
52
+ highestMempoolNonce = tx.nonce;
53
53
  }
54
54
  }
55
- catch {
56
- // Non-fatal: fall back to confirmed nonce only
57
- }
58
55
  const chainNext = Math.max(confirmedNonce, highestMempoolNonce + 1);
59
56
  // 3. Reconcile tracker with chain state
60
57
  await reconcileWithChain(address, chainNext);
@@ -56,21 +56,18 @@ async function getNextNonce(address) {
56
56
  const hiroApi = getHiroApi(NETWORK);
57
57
  const accountInfo = await hiroApi.getAccountInfo(address);
58
58
  const confirmedNonce = accountInfo.nonce;
59
+ // The mempool scan is what makes the nonce safe against pending txs —
60
+ // silently skipping it on failure risks a nonce collision, so let it fail.
59
61
  let highestMempoolNonce = -1;
60
- try {
61
- const mempool = await hiroApi.getMempoolTransactions({
62
- sender_address: address,
63
- limit: 50,
64
- });
65
- for (const tx of mempool.results) {
66
- if (tx.nonce > highestMempoolNonce) {
67
- highestMempoolNonce = tx.nonce;
68
- }
62
+ const mempool = await hiroApi.getMempoolTransactions({
63
+ sender_address: address,
64
+ limit: 50,
65
+ });
66
+ for (const tx of mempool.results) {
67
+ if (tx.nonce > highestMempoolNonce) {
68
+ highestMempoolNonce = tx.nonce;
69
69
  }
70
70
  }
71
- catch {
72
- // Non-fatal
73
- }
74
71
  const chainNext = Math.max(confirmedNonce, highestMempoolNonce + 1);
75
72
  await reconcileWithChain(address, chainNext);
76
73
  return Math.max(chainNext, localNext ?? 0);
@@ -2,7 +2,7 @@ import { z } from "zod";
2
2
  import crypto from "crypto";
3
3
  import { tupleCV, stringAsciiCV, uintCV, principalCV, noneCV, trueCV, falseCV, } from "@stacks/transactions";
4
4
  import { getSigningKeyService, generateAuthId, } from "../services/signing-key.service.js";
5
- import { getPillarApi } from "../services/pillar-api.service.js";
5
+ import { getPillarApi, PillarApiError } from "../services/pillar-api.service.js";
6
6
  import { getHiroApi } from "../services/hiro-api.js";
7
7
  import { NETWORK } from "../config/networks.js";
8
8
  import { PILLAR_API_KEY } from "../config/pillar.js";
@@ -497,8 +497,12 @@ export function registerPillarDirectTools(server) {
497
497
  const walletInfo = await api.get(`/api/smart-wallet/${walletName}`);
498
498
  walletStatus = walletInfo.data?.status || null;
499
499
  }
500
- catch {
501
- // Wallet not found in backend
500
+ catch (error) {
501
+ // 404 = wallet not registered in backend. Anything else (network,
502
+ // 5xx) must surface, or an initialized wallet gets misreported.
503
+ if (!(error instanceof PillarApiError && error.status === 404)) {
504
+ throw error;
505
+ }
502
506
  }
503
507
  if (!walletStatus || walletStatus === "pending_init") {
504
508
  return createJsonResponse({
@@ -554,8 +558,11 @@ export function registerPillarDirectTools(server) {
554
558
  });
555
559
  position = unwindQuote.data;
556
560
  }
557
- catch {
558
- // unwind-quote may fail for fresh wallets with no position
561
+ catch (error) {
562
+ // 4xx = fresh wallet with no position. Infra failures must surface.
563
+ if (!(error instanceof PillarApiError && error.status < 500)) {
564
+ throw error;
565
+ }
559
566
  }
560
567
  return createJsonResponse({
561
568
  success: true,
@@ -1146,21 +1153,16 @@ export function registerPillarDirectTools(server) {
1146
1153
  const frac = (micro % BigInt(1_000_000)).toString().padStart(6, "0");
1147
1154
  return `${whole}.${frac} STX`;
1148
1155
  };
1149
- // Fetch PoX cycle info
1150
- let poxInfo = null;
1151
- try {
1152
- const pox = await hiro.getPoxInfo();
1153
- poxInfo = {
1154
- currentCycleId: pox.current_cycle.id,
1155
- nextCycleId: pox.next_cycle.id,
1156
- blocksUntilNextCycle: pox.next_cycle.blocks_until_reward_phase,
1157
- minAmountUstx: pox.min_amount_ustx,
1158
- isPoxActive: pox.current_cycle.is_pox_active,
1159
- };
1160
- }
1161
- catch {
1162
- // PoX info fetch failed, continue without it
1163
- }
1156
+ // Fetch PoX cycle info — a global endpoint with no "not found" case,
1157
+ // so any failure is infrastructure and should surface.
1158
+ const pox = await hiro.getPoxInfo();
1159
+ const poxInfo = {
1160
+ currentCycleId: pox.current_cycle.id,
1161
+ nextCycleId: pox.next_cycle.id,
1162
+ blocksUntilNextCycle: pox.next_cycle.blocks_until_reward_phase,
1163
+ minAmountUstx: pox.min_amount_ustx,
1164
+ isPoxActive: pox.current_cycle.is_pox_active,
1165
+ };
1164
1166
  // Check enrollment status via backend
1165
1167
  const api = getPillarApi();
1166
1168
  const contractParts = session.smartWallet.split(".");
@@ -1175,8 +1177,13 @@ export function registerPillarDirectTools(server) {
1175
1177
  };
1176
1178
  }
1177
1179
  }
1178
- catch {
1179
- // Backend lookup failed, continue without enrollment info
1180
+ catch (error) {
1181
+ // 404 = wallet not registered, so not enrolled. Other failures
1182
+ // must surface — defaulting to enrolled:false on a backend outage
1183
+ // misreports an enrolled wallet and could prompt re-enrollment.
1184
+ if (!(error instanceof PillarApiError && error.status === 404)) {
1185
+ throw error;
1186
+ }
1180
1187
  }
1181
1188
  const isStacking = lockedMicro > BigInt(0);
1182
1189
  return createJsonResponse({
@@ -283,16 +283,9 @@ Signers later process the request and send BTC on L1.`,
283
283
  description: "Get information about how to deposit BTC to receive sBTC.",
284
284
  }, async () => {
285
285
  try {
286
- // Try to get wallet account (don't throw if not unlocked)
286
+ // getActiveAccount returns null when locked - generic instructions then
287
287
  const walletManager = getWalletManager();
288
- let account;
289
- try {
290
- account = walletManager.getActiveAccount();
291
- }
292
- catch {
293
- // Wallet not unlocked - return generic instructions
294
- account = null;
295
- }
288
+ const account = walletManager.getActiveAccount();
296
289
  // If wallet is unlocked and has Taproot keys, generate real deposit address
297
290
  if (account?.taprootPublicKey) {
298
291
  const depositService = getSbtcDepositService(NETWORK);