@aibtc/mcp-server 1.58.0 → 1.60.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 (265) hide show
  1. package/README.md +41 -0
  2. package/dist/bridge/index.d.ts +31 -0
  3. package/dist/bridge/index.js +285 -0
  4. package/dist/index.js +28 -5
  5. package/dist/services/credentials.js +7 -2
  6. package/dist/services/defi.service.js +23 -11
  7. package/dist/services/hiro-api.d.ts +10 -0
  8. package/dist/services/hiro-api.js +30 -8
  9. package/dist/services/nft.service.js +14 -5
  10. package/dist/services/pillar-api.service.d.ts +8 -0
  11. package/dist/services/pillar-api.service.js +13 -1
  12. package/dist/services/spend-limiter.d.ts +45 -0
  13. package/dist/services/spend-limiter.js +218 -0
  14. package/dist/services/wallet-manager.js +5 -0
  15. package/dist/services/x402.service.js +63 -13
  16. package/dist/tools/bitcoin.tools.js +19 -0
  17. package/dist/tools/endpoint.tools.js +3 -1
  18. package/dist/tools/inbox.tools.js +9 -12
  19. package/dist/tools/news.tools.js +14 -16
  20. package/dist/tools/pillar-direct.tools.js +29 -22
  21. package/dist/tools/sbtc.tools.js +2 -9
  22. package/dist/tools/scaffold.tools.js +9 -14
  23. package/dist/tools/stacking-lottery.tools.js +8 -5
  24. package/dist/tools/yield-dashboard.tools.js +10 -14
  25. package/dist/transactions/builder.js +5 -0
  26. package/dist/utils/encryption.js +2 -0
  27. package/package.json +11 -5
  28. package/skill/SKILL.md +1 -1
  29. package/skill/references/genesis-lifecycle.md +105 -92
  30. package/dist/config/bitcoin-constants.d.ts.map +0 -1
  31. package/dist/config/bitcoin-constants.js.map +0 -1
  32. package/dist/config/caip.d.ts.map +0 -1
  33. package/dist/config/caip.js.map +0 -1
  34. package/dist/config/competition.d.ts.map +0 -1
  35. package/dist/config/competition.js.map +0 -1
  36. package/dist/config/contracts.d.ts.map +0 -1
  37. package/dist/config/contracts.js.map +0 -1
  38. package/dist/config/index.d.ts.map +0 -1
  39. package/dist/config/index.js.map +0 -1
  40. package/dist/config/networks.d.ts.map +0 -1
  41. package/dist/config/networks.js.map +0 -1
  42. package/dist/config/pillar.d.ts.map +0 -1
  43. package/dist/config/pillar.js.map +0 -1
  44. package/dist/config/sponsor.d.ts.map +0 -1
  45. package/dist/config/sponsor.js.map +0 -1
  46. package/dist/endpoints/index.d.ts.map +0 -1
  47. package/dist/endpoints/index.js.map +0 -1
  48. package/dist/endpoints/registry.d.ts.map +0 -1
  49. package/dist/endpoints/registry.js.map +0 -1
  50. package/dist/index.d.ts.map +0 -1
  51. package/dist/index.js.map +0 -1
  52. package/dist/services/bitflow.service.d.ts.map +0 -1
  53. package/dist/services/bitflow.service.js.map +0 -1
  54. package/dist/services/bns.service.d.ts.map +0 -1
  55. package/dist/services/bns.service.js.map +0 -1
  56. package/dist/services/credentials.d.ts.map +0 -1
  57. package/dist/services/credentials.js.map +0 -1
  58. package/dist/services/defi.service.d.ts.map +0 -1
  59. package/dist/services/defi.service.js.map +0 -1
  60. package/dist/services/erc8004.service.d.ts.map +0 -1
  61. package/dist/services/erc8004.service.js.map +0 -1
  62. package/dist/services/hiro-api.d.ts.map +0 -1
  63. package/dist/services/hiro-api.js.map +0 -1
  64. package/dist/services/inscription-parser.d.ts.map +0 -1
  65. package/dist/services/inscription-parser.js.map +0 -1
  66. package/dist/services/lightning/provider.d.ts.map +0 -1
  67. package/dist/services/lightning/provider.js.map +0 -1
  68. package/dist/services/lightning/spark-provider.d.ts.map +0 -1
  69. package/dist/services/lightning/spark-provider.js.map +0 -1
  70. package/dist/services/lightning-manager.d.ts.map +0 -1
  71. package/dist/services/lightning-manager.js.map +0 -1
  72. package/dist/services/mempool-api.d.ts.map +0 -1
  73. package/dist/services/mempool-api.js.map +0 -1
  74. package/dist/services/nft.service.d.ts.map +0 -1
  75. package/dist/services/nft.service.js.map +0 -1
  76. package/dist/services/nonce-tracker.d.ts.map +0 -1
  77. package/dist/services/nonce-tracker.js.map +0 -1
  78. package/dist/services/pillar-api.service.d.ts.map +0 -1
  79. package/dist/services/pillar-api.service.js.map +0 -1
  80. package/dist/services/sbtc-deposit.service.d.ts.map +0 -1
  81. package/dist/services/sbtc-deposit.service.js.map +0 -1
  82. package/dist/services/sbtc.service.d.ts.map +0 -1
  83. package/dist/services/sbtc.service.js.map +0 -1
  84. package/dist/services/scaffold.service.d.ts.map +0 -1
  85. package/dist/services/scaffold.service.js.map +0 -1
  86. package/dist/services/signing-key.service.d.ts.map +0 -1
  87. package/dist/services/signing-key.service.js.map +0 -1
  88. package/dist/services/stacking.service.d.ts.map +0 -1
  89. package/dist/services/stacking.service.js.map +0 -1
  90. package/dist/services/tenero-api.d.ts.map +0 -1
  91. package/dist/services/tenero-api.js.map +0 -1
  92. package/dist/services/tokens.service.d.ts.map +0 -1
  93. package/dist/services/tokens.service.js.map +0 -1
  94. package/dist/services/unisat-indexer.d.ts.map +0 -1
  95. package/dist/services/unisat-indexer.js.map +0 -1
  96. package/dist/services/wallet-manager.d.ts.map +0 -1
  97. package/dist/services/wallet-manager.js.map +0 -1
  98. package/dist/services/x402.service.d.ts.map +0 -1
  99. package/dist/services/x402.service.js.map +0 -1
  100. package/dist/tools/arxiv-research.tools.d.ts.map +0 -1
  101. package/dist/tools/arxiv-research.tools.js.map +0 -1
  102. package/dist/tools/bitcoin.tools.d.ts.map +0 -1
  103. package/dist/tools/bitcoin.tools.js.map +0 -1
  104. package/dist/tools/bitflow.tools.d.ts.map +0 -1
  105. package/dist/tools/bitflow.tools.js.map +0 -1
  106. package/dist/tools/bns.tools.d.ts.map +0 -1
  107. package/dist/tools/bns.tools.js.map +0 -1
  108. package/dist/tools/bounty-scanner.tools.d.ts.map +0 -1
  109. package/dist/tools/bounty-scanner.tools.js.map +0 -1
  110. package/dist/tools/child-inscription.tools.d.ts.map +0 -1
  111. package/dist/tools/child-inscription.tools.js.map +0 -1
  112. package/dist/tools/competition.tools.d.ts.map +0 -1
  113. package/dist/tools/competition.tools.js.map +0 -1
  114. package/dist/tools/contract.tools.d.ts.map +0 -1
  115. package/dist/tools/contract.tools.js.map +0 -1
  116. package/dist/tools/credentials.tools.d.ts.map +0 -1
  117. package/dist/tools/credentials.tools.js.map +0 -1
  118. package/dist/tools/defi.tools.d.ts.map +0 -1
  119. package/dist/tools/defi.tools.js.map +0 -1
  120. package/dist/tools/dual-stacking.tools.d.ts.map +0 -1
  121. package/dist/tools/dual-stacking.tools.js.map +0 -1
  122. package/dist/tools/earning.tools.d.ts.map +0 -1
  123. package/dist/tools/earning.tools.js.map +0 -1
  124. package/dist/tools/endpoint.tools.d.ts.map +0 -1
  125. package/dist/tools/endpoint.tools.js.map +0 -1
  126. package/dist/tools/erc8004.tools.d.ts.map +0 -1
  127. package/dist/tools/erc8004.tools.js.map +0 -1
  128. package/dist/tools/identity.tools.d.ts.map +0 -1
  129. package/dist/tools/identity.tools.js.map +0 -1
  130. package/dist/tools/inbox-x402.tools.d.ts.map +0 -1
  131. package/dist/tools/inbox-x402.tools.js.map +0 -1
  132. package/dist/tools/inbox.tools.d.ts.map +0 -1
  133. package/dist/tools/inbox.tools.js.map +0 -1
  134. package/dist/tools/index.d.ts.map +0 -1
  135. package/dist/tools/index.js.map +0 -1
  136. package/dist/tools/jingswap.tools.d.ts.map +0 -1
  137. package/dist/tools/jingswap.tools.js.map +0 -1
  138. package/dist/tools/lightning.tools.d.ts.map +0 -1
  139. package/dist/tools/lightning.tools.js.map +0 -1
  140. package/dist/tools/mempool.tools.d.ts.map +0 -1
  141. package/dist/tools/mempool.tools.js.map +0 -1
  142. package/dist/tools/news.tools.d.ts.map +0 -1
  143. package/dist/tools/news.tools.js.map +0 -1
  144. package/dist/tools/nft.tools.d.ts.map +0 -1
  145. package/dist/tools/nft.tools.js.map +0 -1
  146. package/dist/tools/nonce.tools.d.ts.map +0 -1
  147. package/dist/tools/nonce.tools.js.map +0 -1
  148. package/dist/tools/nostr.tools.d.ts.map +0 -1
  149. package/dist/tools/nostr.tools.js.map +0 -1
  150. package/dist/tools/openrouter.tools.d.ts.map +0 -1
  151. package/dist/tools/openrouter.tools.js.map +0 -1
  152. package/dist/tools/ordinals-marketplace.tools.d.ts.map +0 -1
  153. package/dist/tools/ordinals-marketplace.tools.js.map +0 -1
  154. package/dist/tools/ordinals-p2p.tools.d.ts.map +0 -1
  155. package/dist/tools/ordinals-p2p.tools.js.map +0 -1
  156. package/dist/tools/ordinals.tools.d.ts.map +0 -1
  157. package/dist/tools/ordinals.tools.js.map +0 -1
  158. package/dist/tools/pillar-direct.tools.d.ts.map +0 -1
  159. package/dist/tools/pillar-direct.tools.js.map +0 -1
  160. package/dist/tools/pillar.tools.d.ts.map +0 -1
  161. package/dist/tools/pillar.tools.js.map +0 -1
  162. package/dist/tools/psbt.helpers.d.ts.map +0 -1
  163. package/dist/tools/psbt.helpers.js.map +0 -1
  164. package/dist/tools/psbt.tools.d.ts.map +0 -1
  165. package/dist/tools/psbt.tools.js.map +0 -1
  166. package/dist/tools/query.tools.d.ts.map +0 -1
  167. package/dist/tools/query.tools.js.map +0 -1
  168. package/dist/tools/relay-diagnostic.tools.d.ts.map +0 -1
  169. package/dist/tools/relay-diagnostic.tools.js.map +0 -1
  170. package/dist/tools/reputation.tools.d.ts.map +0 -1
  171. package/dist/tools/reputation.tools.js.map +0 -1
  172. package/dist/tools/runes.tools.d.ts.map +0 -1
  173. package/dist/tools/runes.tools.js.map +0 -1
  174. package/dist/tools/sbtc.tools.d.ts.map +0 -1
  175. package/dist/tools/sbtc.tools.js.map +0 -1
  176. package/dist/tools/scaffold.tools.d.ts.map +0 -1
  177. package/dist/tools/scaffold.tools.js.map +0 -1
  178. package/dist/tools/schemas.d.ts.map +0 -1
  179. package/dist/tools/schemas.js.map +0 -1
  180. package/dist/tools/settings.tools.d.ts.map +0 -1
  181. package/dist/tools/settings.tools.js.map +0 -1
  182. package/dist/tools/signing.tools.d.ts.map +0 -1
  183. package/dist/tools/signing.tools.js.map +0 -1
  184. package/dist/tools/skill-mappings.d.ts.map +0 -1
  185. package/dist/tools/skill-mappings.js.map +0 -1
  186. package/dist/tools/souldinals.tools.d.ts.map +0 -1
  187. package/dist/tools/souldinals.tools.js.map +0 -1
  188. package/dist/tools/stacking-lottery.tools.d.ts.map +0 -1
  189. package/dist/tools/stacking-lottery.tools.js.map +0 -1
  190. package/dist/tools/stacking.tools.d.ts.map +0 -1
  191. package/dist/tools/stacking.tools.js.map +0 -1
  192. package/dist/tools/stacks-market.tools.d.ts.map +0 -1
  193. package/dist/tools/stacks-market.tools.js.map +0 -1
  194. package/dist/tools/styx.tools.d.ts.map +0 -1
  195. package/dist/tools/styx.tools.js.map +0 -1
  196. package/dist/tools/taproot-multisig.tools.d.ts.map +0 -1
  197. package/dist/tools/taproot-multisig.tools.js.map +0 -1
  198. package/dist/tools/tenero.tools.d.ts.map +0 -1
  199. package/dist/tools/tenero.tools.js.map +0 -1
  200. package/dist/tools/tokens.tools.d.ts.map +0 -1
  201. package/dist/tools/tokens.tools.js.map +0 -1
  202. package/dist/tools/transfer.tools.d.ts.map +0 -1
  203. package/dist/tools/transfer.tools.js.map +0 -1
  204. package/dist/tools/wallet-management.tools.d.ts.map +0 -1
  205. package/dist/tools/wallet-management.tools.js.map +0 -1
  206. package/dist/tools/wallet.tools.d.ts.map +0 -1
  207. package/dist/tools/wallet.tools.js.map +0 -1
  208. package/dist/tools/yield-dashboard.tools.d.ts.map +0 -1
  209. package/dist/tools/yield-dashboard.tools.js.map +0 -1
  210. package/dist/tools/yield-hunter.tools.d.ts.map +0 -1
  211. package/dist/tools/yield-hunter.tools.js.map +0 -1
  212. package/dist/transactions/bitcoin-builder.d.ts.map +0 -1
  213. package/dist/transactions/bitcoin-builder.js.map +0 -1
  214. package/dist/transactions/builder.d.ts.map +0 -1
  215. package/dist/transactions/builder.js.map +0 -1
  216. package/dist/transactions/child-inscription-builder.d.ts.map +0 -1
  217. package/dist/transactions/child-inscription-builder.js.map +0 -1
  218. package/dist/transactions/clarity-values.d.ts.map +0 -1
  219. package/dist/transactions/clarity-values.js.map +0 -1
  220. package/dist/transactions/inscription-builder.d.ts.map +0 -1
  221. package/dist/transactions/inscription-builder.js.map +0 -1
  222. package/dist/transactions/post-conditions.d.ts.map +0 -1
  223. package/dist/transactions/post-conditions.js.map +0 -1
  224. package/dist/transactions/rune-transfer-builder.d.ts.map +0 -1
  225. package/dist/transactions/rune-transfer-builder.js.map +0 -1
  226. package/dist/transactions/runestone-builder.d.ts.map +0 -1
  227. package/dist/transactions/runestone-builder.js.map +0 -1
  228. package/dist/transactions/sponsor-builder.d.ts.map +0 -1
  229. package/dist/transactions/sponsor-builder.js.map +0 -1
  230. package/dist/utils/bip322.d.ts.map +0 -1
  231. package/dist/utils/bip322.js.map +0 -1
  232. package/dist/utils/bitcoin.d.ts.map +0 -1
  233. package/dist/utils/bitcoin.js.map +0 -1
  234. package/dist/utils/encryption.d.ts.map +0 -1
  235. package/dist/utils/encryption.js.map +0 -1
  236. package/dist/utils/erc8004-helpers.d.ts.map +0 -1
  237. package/dist/utils/erc8004-helpers.js.map +0 -1
  238. package/dist/utils/errors.d.ts.map +0 -1
  239. package/dist/utils/errors.js.map +0 -1
  240. package/dist/utils/fee.d.ts.map +0 -1
  241. package/dist/utils/fee.js.map +0 -1
  242. package/dist/utils/formatting.d.ts.map +0 -1
  243. package/dist/utils/formatting.js.map +0 -1
  244. package/dist/utils/index.d.ts.map +0 -1
  245. package/dist/utils/index.js.map +0 -1
  246. package/dist/utils/l402-protocol.d.ts.map +0 -1
  247. package/dist/utils/l402-protocol.js.map +0 -1
  248. package/dist/utils/redact.d.ts.map +0 -1
  249. package/dist/utils/redact.js.map +0 -1
  250. package/dist/utils/relay-health.d.ts.map +0 -1
  251. package/dist/utils/relay-health.js.map +0 -1
  252. package/dist/utils/storage.d.ts.map +0 -1
  253. package/dist/utils/storage.js.map +0 -1
  254. package/dist/utils/tokens.d.ts.map +0 -1
  255. package/dist/utils/tokens.js.map +0 -1
  256. package/dist/utils/x402-payment-logging.d.ts.map +0 -1
  257. package/dist/utils/x402-payment-logging.js.map +0 -1
  258. package/dist/utils/x402-payment-state.d.ts.map +0 -1
  259. package/dist/utils/x402-payment-state.js.map +0 -1
  260. package/dist/utils/x402-protocol.d.ts.map +0 -1
  261. package/dist/utils/x402-protocol.js.map +0 -1
  262. package/dist/utils/x402-recovery.d.ts.map +0 -1
  263. package/dist/utils/x402-recovery.js.map +0 -1
  264. package/dist/yield-hunter/index.d.ts.map +0 -1
  265. package/dist/yield-hunter/index.js.map +0 -1
@@ -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
  }
@@ -0,0 +1,45 @@
1
+ export type SpendUnit = "ustx" | "sats";
2
+ export declare class SpendLimitError extends Error {
3
+ readonly unit: SpendUnit;
4
+ readonly attempted: number;
5
+ readonly remaining: number;
6
+ readonly scope: "session" | "day";
7
+ constructor(message: string, unit: SpendUnit, attempted: number, remaining: number, scope: "session" | "day");
8
+ }
9
+ declare class SpendLimiter {
10
+ private static instance;
11
+ private stateFile;
12
+ private session;
13
+ private writeLock;
14
+ static getInstance(): SpendLimiter;
15
+ /** Test seam: redirect the state file. */
16
+ setStateFile(file: string): void;
17
+ private readState;
18
+ private writeState;
19
+ private getSession;
20
+ /** Reset the in-memory session ledger for a wallet (call on unlock/lock/switch). */
21
+ resetSession(addr: string): void;
22
+ /**
23
+ * Throw SpendLimitError if `amount` would push the session OR day total over
24
+ * its cap. Does NOT record — call recordSpend after a successful broadcast.
25
+ */
26
+ check(unit: SpendUnit, amount: bigint, addr: string): Promise<void>;
27
+ /** Record a completed spend against both the session and day ledgers. */
28
+ record(unit: SpendUnit, amount: bigint, addr: string): Promise<void>;
29
+ /** Remaining session/day budget for status reporting. */
30
+ status(addr: string): Promise<{
31
+ enabled: boolean;
32
+ ustx: {
33
+ sessionRemaining: number;
34
+ dailyRemaining: number;
35
+ };
36
+ sats: {
37
+ sessionRemaining: number;
38
+ dailyRemaining: number;
39
+ };
40
+ }>;
41
+ private message;
42
+ }
43
+ export declare function getSpendLimiter(): SpendLimiter;
44
+ export {};
45
+ //# sourceMappingURL=spend-limiter.d.ts.map
@@ -0,0 +1,218 @@
1
+ /**
2
+ * Default-on cumulative spending limit for the wallet.
3
+ *
4
+ * The MCP server lets an LLM move real funds with no human click-through, so a
5
+ * single poisoned instruction (or a malicious x402 endpoint) could otherwise
6
+ * drain the wallet. This module is the safety rail: every outbound spend path
7
+ * (native STX transfer, BTC L1 transfer, x402/L402 auto-payments, Lightning
8
+ * pays) routes through it, and the limiter blocks once cumulative outflow would
9
+ * exceed a per-session OR per-day cap.
10
+ *
11
+ * Two independent ledgers are tracked — micro-STX (`ustx`) and satoshis
12
+ * (`sats`); there is no cross-asset USD normalization. Daily totals are
13
+ * persisted to ~/.aibtc/spend-state.json (keyed by wallet address + UTC day) so
14
+ * the cap survives process restarts; session totals live in memory and reset on
15
+ * wallet unlock/lock/switch.
16
+ *
17
+ * Exceeding the cap is the intended human-in-the-loop checkpoint: it blocks and
18
+ * surfaces the remaining budget + the env var to raise. An LLM cannot set env
19
+ * vars mid-session, so it cannot raise its own ceiling.
20
+ *
21
+ * Disable entirely with SPEND_LIMIT_ENABLED=false. Override caps with
22
+ * SPEND_LIMIT_DAILY_USTX / _SESSION_USTX / _DAILY_SATS / _SESSION_SATS.
23
+ */
24
+ import { promises as fs } from "fs";
25
+ import os from "os";
26
+ import path from "path";
27
+ const STORAGE_DIR = path.join(os.homedir(), ".aibtc");
28
+ const DEFAULT_STATE_FILE = path.join(STORAGE_DIR, "spend-state.json");
29
+ /**
30
+ * Parse a positive-integer env override, falling back (with a warning) on any
31
+ * invalid value — never silently disable a cap. Mirrors parseSatsCap in
32
+ * x402.service.ts.
33
+ */
34
+ function parseLimit(envName, fallback) {
35
+ const raw = process.env[envName];
36
+ if (raw === undefined || raw === "")
37
+ return fallback;
38
+ const parsed = Number(raw);
39
+ if (!Number.isFinite(parsed) || parsed <= 0) {
40
+ console.error(`[spend-limit] Invalid ${envName}="${raw}", falling back to ${fallback}`);
41
+ return fallback;
42
+ }
43
+ return Math.floor(parsed);
44
+ }
45
+ function isEnabled() {
46
+ return process.env.SPEND_LIMIT_ENABLED !== "false";
47
+ }
48
+ // Conservative defaults (~$30/day at the time of writing): tight enough to stop
49
+ // a drain, generous for the micro-payment use case (registry endpoints cost
50
+ // <=0.02 STX / 100 sats). Raise per wallet via env.
51
+ const DEFAULTS = {
52
+ dailyUstx: 10_000_000, // 10 STX
53
+ sessionUstx: 10_000_000, // 10 STX
54
+ dailySats: 50_000, // ~$30 BTC
55
+ sessionSats: 50_000,
56
+ };
57
+ function capsFor(unit) {
58
+ if (unit === "ustx") {
59
+ return {
60
+ daily: parseLimit("SPEND_LIMIT_DAILY_USTX", DEFAULTS.dailyUstx),
61
+ session: parseLimit("SPEND_LIMIT_SESSION_USTX", DEFAULTS.sessionUstx),
62
+ };
63
+ }
64
+ return {
65
+ daily: parseLimit("SPEND_LIMIT_DAILY_SATS", DEFAULTS.dailySats),
66
+ session: parseLimit("SPEND_LIMIT_SESSION_SATS", DEFAULTS.sessionSats),
67
+ };
68
+ }
69
+ /** UTC day bucket, e.g. "2026-06-11". */
70
+ function todayKey() {
71
+ return new Date().toISOString().slice(0, 10);
72
+ }
73
+ function unitLabel(unit) {
74
+ return unit === "ustx" ? "uSTX" : "sats";
75
+ }
76
+ export class SpendLimitError extends Error {
77
+ unit;
78
+ attempted;
79
+ remaining;
80
+ scope;
81
+ constructor(message, unit, attempted, remaining, scope) {
82
+ super(message);
83
+ this.unit = unit;
84
+ this.attempted = attempted;
85
+ this.remaining = remaining;
86
+ this.scope = scope;
87
+ this.name = "SpendLimitError";
88
+ }
89
+ }
90
+ class SpendLimiter {
91
+ static instance;
92
+ stateFile = DEFAULT_STATE_FILE;
93
+ // In-memory per-session totals, keyed by wallet address.
94
+ session = new Map();
95
+ writeLock = Promise.resolve();
96
+ static getInstance() {
97
+ if (!SpendLimiter.instance)
98
+ SpendLimiter.instance = new SpendLimiter();
99
+ return SpendLimiter.instance;
100
+ }
101
+ /** Test seam: redirect the state file. */
102
+ setStateFile(file) {
103
+ this.stateFile = file;
104
+ }
105
+ async readState() {
106
+ try {
107
+ const content = await fs.readFile(this.stateFile, "utf8");
108
+ return JSON.parse(content);
109
+ }
110
+ catch {
111
+ return {};
112
+ }
113
+ }
114
+ async writeState(state) {
115
+ const dir = path.dirname(this.stateFile);
116
+ await fs.mkdir(dir, { recursive: true, mode: 0o700 });
117
+ const tmp = `${this.stateFile}.tmp`;
118
+ await fs.writeFile(tmp, JSON.stringify(state, null, 2), { mode: 0o600 });
119
+ await fs.rename(tmp, this.stateFile);
120
+ }
121
+ getSession(addr) {
122
+ let s = this.session.get(addr);
123
+ if (!s) {
124
+ s = { ustx: 0, sats: 0 };
125
+ this.session.set(addr, s);
126
+ }
127
+ return s;
128
+ }
129
+ /** Reset the in-memory session ledger for a wallet (call on unlock/lock/switch). */
130
+ resetSession(addr) {
131
+ this.session.delete(addr);
132
+ }
133
+ /**
134
+ * Throw SpendLimitError if `amount` would push the session OR day total over
135
+ * its cap. Does NOT record — call recordSpend after a successful broadcast.
136
+ */
137
+ async check(unit, amount, addr) {
138
+ if (!isEnabled())
139
+ return;
140
+ if (amount <= 0n)
141
+ return;
142
+ const amt = Number(amount);
143
+ const { daily, session } = capsFor(unit);
144
+ const sessionSpent = this.getSession(addr)[unit];
145
+ if (sessionSpent + amt > session) {
146
+ const remaining = Math.max(0, session - sessionSpent);
147
+ throw new SpendLimitError(this.message(unit, amt, remaining, "session", session), unit, amt, remaining, "session");
148
+ }
149
+ const state = await this.readState();
150
+ const daySpent = state[addr]?.[todayKey()]?.[unit] ?? 0;
151
+ if (daySpent + amt > daily) {
152
+ const remaining = Math.max(0, daily - daySpent);
153
+ throw new SpendLimitError(this.message(unit, amt, remaining, "day", daily), unit, amt, remaining, "day");
154
+ }
155
+ }
156
+ /** Record a completed spend against both the session and day ledgers. */
157
+ async record(unit, amount, addr) {
158
+ if (!isEnabled())
159
+ return;
160
+ if (amount <= 0n)
161
+ return;
162
+ const amt = Number(amount);
163
+ this.getSession(addr)[unit] += amt;
164
+ // Serialize read-modify-write to avoid lost updates under concurrency.
165
+ this.writeLock = this.writeLock.then(async () => {
166
+ const state = await this.readState();
167
+ const today = todayKey();
168
+ // Prune stale days so the file does not grow unbounded.
169
+ for (const a of Object.keys(state)) {
170
+ for (const day of Object.keys(state[a])) {
171
+ if (day !== today)
172
+ delete state[a][day];
173
+ }
174
+ if (Object.keys(state[a]).length === 0)
175
+ delete state[a];
176
+ }
177
+ if (!state[addr])
178
+ state[addr] = {};
179
+ if (!state[addr][today])
180
+ state[addr][today] = { ustx: 0, sats: 0 };
181
+ state[addr][today][unit] += amt;
182
+ await this.writeState(state);
183
+ });
184
+ await this.writeLock;
185
+ }
186
+ /** Remaining session/day budget for status reporting. */
187
+ async status(addr) {
188
+ const state = await this.readState();
189
+ const mk = (unit) => {
190
+ const { daily, session } = capsFor(unit);
191
+ const sessionSpent = this.session.get(addr)?.[unit] ?? 0;
192
+ const daySpent = state[addr]?.[todayKey()]?.[unit] ?? 0;
193
+ return {
194
+ sessionRemaining: Math.max(0, session - sessionSpent),
195
+ dailyRemaining: Math.max(0, daily - daySpent),
196
+ };
197
+ };
198
+ return { enabled: isEnabled(), ustx: mk("ustx"), sats: mk("sats") };
199
+ }
200
+ message(unit, attempted, remaining, scope, cap) {
201
+ const envVar = unit === "ustx"
202
+ ? scope === "day"
203
+ ? "SPEND_LIMIT_DAILY_USTX"
204
+ : "SPEND_LIMIT_SESSION_USTX"
205
+ : scope === "day"
206
+ ? "SPEND_LIMIT_DAILY_SATS"
207
+ : "SPEND_LIMIT_SESSION_SATS";
208
+ const label = unitLabel(unit);
209
+ return (`Spending limit reached: this ${attempted} ${label} transfer would exceed the ` +
210
+ `per-${scope} cap of ${cap} ${label} (${remaining} ${label} remaining). ` +
211
+ `This is a safety rail against draining the wallet. To allow a larger spend, ` +
212
+ `raise ${envVar} (or set SPEND_LIMIT_ENABLED=false to disable), then retry.`);
213
+ }
214
+ }
215
+ export function getSpendLimiter() {
216
+ return SpendLimiter.getInstance();
217
+ }
218
+ //# sourceMappingURL=spend-limiter.js.map
@@ -5,6 +5,7 @@ import { encrypt, decrypt, generateWalletId, initializeStorage, readWalletIndex,
5
5
  import { WalletError, WalletNotFoundError, InvalidPasswordError, InvalidMnemonicError, } from "../utils/errors.js";
6
6
  import { NETWORK } from "../config/networks.js";
7
7
  import { resetPendingNonce } from "../transactions/builder.js";
8
+ import { getSpendLimiter } from "./spend-limiter.js";
8
9
  import { deriveBitcoinAddress, deriveBitcoinKeyPair, deriveTaprootAddress, deriveTaprootKeyPair, deriveNostrKeyPair } from "../utils/bitcoin.js";
9
10
  /**
10
11
  * Wallet manager singleton - handles wallet creation, encryption, and session management
@@ -170,6 +171,8 @@ class WalletManager {
170
171
  await writeAppConfig(config);
171
172
  // Reset pending nonce counter so the new session re-syncs with the chain
172
173
  await resetPendingNonce(account.address);
174
+ // Fresh unlock starts a fresh per-session spend budget.
175
+ getSpendLimiter().resetSession(account.address);
173
176
  return account;
174
177
  }
175
178
  /**
@@ -180,6 +183,8 @@ class WalletManager {
180
183
  // Reset pending nonce counter before clearing session
181
184
  if (this.session?.account) {
182
185
  await resetPendingNonce(this.session.account.address);
186
+ // Clear the per-session spend budget so the next unlock starts fresh.
187
+ getSpendLimiter().resetSession(this.session.account.address);
183
188
  }
184
189
  // Zero out sensitive key buffers before dropping references
185
190
  if (this.session?.account) {
@@ -1,16 +1,18 @@
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";
6
6
  import { getNetworkFromStacksChainId } from "../config/caip.js";
7
7
  import { getWalletManager } from "./wallet-manager.js";
8
+ import { getSpendLimiter } from "./spend-limiter.js";
8
9
  import { formatStx, formatSbtc } from "../utils/formatting.js";
9
10
  import { getSbtcService } from "./sbtc.service.js";
10
11
  import { getHiroApi } from "./hiro-api.js";
11
12
  import { createHash } from "crypto";
12
13
  import { InsufficientBalanceError } from "../utils/errors.js";
13
14
  import { getContracts, parseContractId } from "../config/contracts.js";
15
+ import { resolveDefaultFee } from "../utils/fee.js";
14
16
  import { classifyCanonicalPaymentStatus, formatCanonicalPaymentStatus, resolveCanonicalPaymentStatus, } from "../utils/x402-payment-state.js";
15
17
  import { extractPaymentIdFromPaymentSignature } from "../utils/x402-recovery.js";
16
18
  import { emitCanonicalPaymentDecisionLogs, emitCanonicalPaymentPollLogs, emitPaymentLog, } from "../utils/x402-payment-logging.js";
@@ -42,6 +44,16 @@ function parseSatsCap(envName, fallback) {
42
44
  return parsed;
43
45
  }
44
46
  const L402_MAX_SATS_PER_INVOICE = parseSatsCap("L402_MAX_SATS_PER_INVOICE", DEFAULT_L402_MAX_SATS);
47
+ /**
48
+ * Per-payment caps for the Stacks x402 interceptor, mirroring the L402 cap
49
+ * above: a malicious endpoint can demand an arbitrary amount in its 402
50
+ * response and an autoApprove'd call would pay it, bounded only by wallet
51
+ * balance. Known registry endpoints cost at most 0.02 STX / 100 sats, so the
52
+ * defaults leave generous headroom. Overridable via env vars.
53
+ */
54
+ const X402_MAX_USTX_PER_PAYMENT = parseSatsCap("X402_MAX_USTX_PER_PAYMENT", 1_000_000 // 1 STX
55
+ );
56
+ const X402_MAX_SATS_PER_PAYMENT = parseSatsCap("X402_MAX_SATS_PER_PAYMENT", 10_000);
45
57
  // Track payment attempts per client instance (auto-cleanup via WeakMap)
46
58
  const paymentAttempts = new WeakMap();
47
59
  // Track 429 retry counts per client instance
@@ -354,6 +366,13 @@ export async function createApiClient(baseUrl, options) {
354
366
  `of ${L402_MAX_SATS_PER_INVOICE} sats (L402_MAX_SATS_PER_INVOICE). ` +
355
367
  `Refusing to pay.`));
356
368
  }
369
+ // Cumulative spending limit (sats ledger) — bounds a drain-by-loop of
370
+ // sub-cap L402 invoices. Keyed by the wallet's Stacks address so all sats
371
+ // spends (BTC L1, sBTC, L402) share one ledger.
372
+ const l402Addr = getWalletManager().getActiveAccount()?.address;
373
+ if (l402Addr) {
374
+ await getSpendLimiter().check("sats", BigInt(amountSats), l402Addr);
375
+ }
357
376
  // Pay the Lightning invoice.
358
377
  let payment;
359
378
  try {
@@ -362,6 +381,9 @@ export async function createApiClient(baseUrl, options) {
362
381
  catch (payErr) {
363
382
  return Promise.reject(new Error(`L402 payment failed: ${payErr instanceof Error ? payErr.message : String(payErr)}`));
364
383
  }
384
+ if (l402Addr) {
385
+ await getSpendLimiter().record("sats", BigInt(amountSats), l402Addr);
386
+ }
365
387
  cacheL402Auth(method, canonicalUrlKey, challenge.macaroon, payment.preimage);
366
388
  const originalRequest = error.config;
367
389
  originalRequest.headers = originalRequest.headers || {};
@@ -457,6 +479,16 @@ export async function createApiClient(baseUrl, options) {
457
479
  const networks = paymentRequired.accepts.map((a) => a.network).join(", ");
458
480
  return Promise.reject(new Error(`No compatible Stacks payment option found. Available networks: ${networks}`));
459
481
  }
482
+ const tokenType = detectTokenType(selectedOption.asset);
483
+ const amount = BigInt(selectedOption.amount);
484
+ const isSbtc = tokenType === "sBTC";
485
+ const cap = isSbtc ? X402_MAX_SATS_PER_PAYMENT : X402_MAX_USTX_PER_PAYMENT;
486
+ const unit = isSbtc ? "sats" : "uSTX";
487
+ if (amount > BigInt(Math.floor(cap))) {
488
+ return Promise.reject(new Error(`x402 payment of ${amount} ${unit} exceeds the per-payment cap of ${cap} ${unit}. ` +
489
+ `If this cost is expected, raise the cap via the ` +
490
+ `${isSbtc ? "X402_MAX_SATS_PER_PAYMENT" : "X402_MAX_USTX_PER_PAYMENT"} env var.`));
491
+ }
460
492
  // Lazy-load account on first 402 — free endpoints never reach here
461
493
  const acct = await ensureAccount();
462
494
  // Verify the payment network matches our configured network
@@ -465,6 +497,10 @@ export async function createApiClient(baseUrl, options) {
465
497
  return Promise.reject(new Error(`Network mismatch: endpoint requires ${paymentNetwork} but wallet is configured for ${acct.network}. ` +
466
498
  `Switch to a ${paymentNetwork} wallet or use a ${acct.network} endpoint.`));
467
499
  }
500
+ // Cumulative spending limit: the per-payment cap above bounds a single
501
+ // 402, but a malicious endpoint can loop sub-cap payments. This blocks
502
+ // once the session/day total would be exceeded.
503
+ await getSpendLimiter().check(isSbtc ? "sats" : "ustx", amount, acct.address);
468
504
  // Invoke pre-payment callback (e.g. balance check) before signing/broadcasting.
469
505
  // If the callback throws, the payment is aborted and the error propagates to the caller.
470
506
  if (options?.onBeforePayment) {
@@ -476,9 +512,11 @@ export async function createApiClient(baseUrl, options) {
476
512
  account: acct,
477
513
  });
478
514
  }
479
- // Build a sponsored signed transaction (relay pays gas; fee: 0n)
480
- const tokenType = detectTokenType(selectedOption.asset);
481
- const amount = BigInt(selectedOption.amount);
515
+ // Build a non-sponsored signed transaction: the sender pays its own
516
+ // STX gas (fee + nonce auto-estimated by @stacks), and the x402
517
+ // facilitator simply verifies and broadcasts it. No relay sponsorship
518
+ // — this is the same tx shape the x402-stacks lib produces, and the
519
+ // balance check in onBeforePayment already accounts for the gas.
482
520
  const networkName = getStacksNetwork(acct.network);
483
521
  let transaction;
484
522
  if (tokenType === "sBTC") {
@@ -496,9 +534,18 @@ export async function createApiClient(baseUrl, options) {
496
534
  ],
497
535
  senderKey: acct.privateKey,
498
536
  network: networkName,
499
- postConditionMode: PostConditionMode.Allow,
500
- sponsored: true,
501
- fee: 0n,
537
+ // The payment amount is known at build time, so pin it: a
538
+ // compromised token contract must not be able to pull more.
539
+ postConditionMode: PostConditionMode.Deny,
540
+ postConditions: [
541
+ Pc.principal(acct.address)
542
+ .willSendEq(amount)
543
+ .ft(contracts.SBTC_TOKEN, "sbtc-token"),
544
+ ],
545
+ // Clamped medium fee — do NOT let @stacks auto-estimate, since
546
+ // Hiro's contract_call high_priority tier is polluted by outliers
547
+ // (observed >2000 STX). resolveDefaultFee caps it at 0.05 STX.
548
+ fee: await resolveDefaultFee(acct.network, "contract_call"),
502
549
  });
503
550
  }
504
551
  else {
@@ -508,8 +555,7 @@ export async function createApiClient(baseUrl, options) {
508
555
  senderKey: acct.privateKey,
509
556
  network: networkName,
510
557
  memo: "",
511
- sponsored: true,
512
- fee: 0n,
558
+ fee: await resolveDefaultFee(acct.network, "token_transfer"),
513
559
  });
514
560
  }
515
561
  const txHex = "0x" + transaction.serialize();
@@ -530,6 +576,9 @@ export async function createApiClient(baseUrl, options) {
530
576
  action: "payment_submitted",
531
577
  compatShimUsed: false,
532
578
  });
579
+ // Record the spend against the cumulative ledger now that the signed
580
+ // payment tx is submitted to the facilitator.
581
+ await getSpendLimiter().record(isSbtc ? "sats" : "ustx", amount, acct.address);
533
582
  // Retry the original request with the payment header
534
583
  const originalRequest = error.config;
535
584
  originalRequest.headers = originalRequest.headers || {};
@@ -716,8 +765,9 @@ export async function checkSufficientBalance(account, amount, asset, sponsored =
716
765
  const hiroApiForSbtc = getHiroApi(account.network);
717
766
  const stxInfoForSbtc = await hiroApiForSbtc.getStxBalance(account.address);
718
767
  const stxBalanceForSbtc = BigInt(stxInfoForSbtc.balance);
719
- const sbtcFees = await hiroApiForSbtc.getMempoolFees();
720
- const estimatedSbtcFee = BigInt(sbtcFees.contract_call.high_priority);
768
+ // Same clamped fee the interceptor actually sets on the sBTC contract
769
+ // call — so this check is exact, not a high-priority over-estimate.
770
+ const estimatedSbtcFee = await resolveDefaultFee(account.network, "contract_call");
721
771
  if (stxBalanceForSbtc < estimatedSbtcFee) {
722
772
  const stxShortfall = estimatedSbtcFee - stxBalanceForSbtc;
723
773
  throw new InsufficientBalanceError(`Insufficient STX balance to cover sBTC transfer fee: need ${formatStx(estimatedSbtcFee.toString())} estimated fee, ` +
@@ -733,8 +783,8 @@ export async function checkSufficientBalance(account, amount, asset, sponsored =
733
783
  const balance = BigInt(balanceInfo.balance);
734
784
  let totalRequired = requiredAmount;
735
785
  if (!sponsored) {
736
- const mempoolFees = await hiroApi.getMempoolFees();
737
- const estimatedFee = BigInt(mempoolFees.contract_call.high_priority);
786
+ // Same clamped fee the interceptor sets on the STX transfer.
787
+ const estimatedFee = await resolveDefaultFee(account.network, "token_transfer");
738
788
  totalRequired = requiredAmount + estimatedFee;
739
789
  }
740
790
  if (balance >= totalRequired)
@@ -14,6 +14,7 @@ import { z } from "zod";
14
14
  import { NETWORK } from "../config/networks.js";
15
15
  import { createJsonResponse, createErrorResponse } from "../utils/index.js";
16
16
  import { getWalletManager } from "../services/wallet-manager.js";
17
+ import { getSpendLimiter } from "../services/spend-limiter.js";
17
18
  import { MempoolApi, getMempoolAddressUrl, getMempoolTxUrl, } from "../services/mempool-api.js";
18
19
  import { buildAndSignBtcTransaction } from "../transactions/bitcoin-builder.js";
19
20
  import { UnisatIndexer } from "../services/unisat-indexer.js";
@@ -33,6 +34,19 @@ async function getBtcAddress(providedAddress) {
33
34
  throw new Error("No Bitcoin address provided and wallet is not unlocked. " +
34
35
  "Either provide an address or unlock your wallet first.");
35
36
  }
37
+ /**
38
+ * Reject recipient addresses that belong to the other Bitcoin network before
39
+ * any UTXO fetching. The tx builder (@scure/btc-signer) rejects these too, but
40
+ * late and with a cryptic bech32/version-byte error.
41
+ */
42
+ function validateRecipientNetwork(address) {
43
+ const prefixes = NETWORK === "mainnet" ? ["bc1", "1", "3"] : ["tb1", "m", "n", "2"];
44
+ if (!prefixes.some((p) => address.startsWith(p))) {
45
+ throw new Error(`Recipient "${address}" is not a ${NETWORK} address. ` +
46
+ `Expected a prefix of ${prefixes.join(", ")} — check that the address ` +
47
+ `matches the configured NETWORK.`);
48
+ }
49
+ }
36
50
  /**
37
51
  * Format satoshis as BTC string
38
52
  */
@@ -231,12 +245,16 @@ export function registerBitcoinTools(server) {
231
245
  },
232
246
  }, async ({ recipient, amount, feeRate, includeOrdinals }) => {
233
247
  try {
248
+ validateRecipientNetwork(recipient);
234
249
  // Get wallet account (requires unlocked wallet)
235
250
  const walletManager = getWalletManager();
236
251
  const account = walletManager.getActiveAccount();
237
252
  if (!account) {
238
253
  throw new Error("Wallet is not unlocked. Use wallet_unlock first to enable transactions.");
239
254
  }
255
+ // Safety rail: block before signing if this would exceed the wallet's
256
+ // cumulative spending limit (per-session or per-day).
257
+ await getSpendLimiter().check("sats", BigInt(amount), account.address);
240
258
  if (!account.btcAddress || !account.btcPrivateKey || !account.btcPublicKey) {
241
259
  throw new Error("Bitcoin keys not available. Please unlock your wallet again.");
242
260
  }
@@ -294,6 +312,7 @@ export function registerBitcoinTools(server) {
294
312
  }, account.btcPrivateKey);
295
313
  // Broadcast the transaction
296
314
  const txid = await api.broadcastTransaction(txResult.txHex);
315
+ await getSpendLimiter().record("sats", BigInt(amount), account.address);
297
316
  const response = {
298
317
  success: true,
299
318
  txid,
@@ -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);