@aibtc/mcp-server 1.67.0 → 1.69.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.
@@ -10,6 +10,7 @@
10
10
  *
11
11
  * Uses micro-ordinals library to parse and create inscriptions.
12
12
  */
13
+ import { createHash } from "node:crypto";
13
14
  import { z } from "zod";
14
15
  import { NETWORK } from "../config/networks.js";
15
16
  import { P2WPKH_INPUT_VBYTES, P2WPKH_OUTPUT_VBYTES, P2TR_OUTPUT_VBYTES, TX_OVERHEAD_VBYTES, DUST_THRESHOLD, P2TR_INPUT_BASE_VBYTES, WITNESS_OVERHEAD_VBYTES, } from "../config/bitcoin-constants.js";
@@ -223,8 +224,10 @@ export function registerOrdinalsTools(server) {
223
224
  feeRate: actualFeeRate,
224
225
  contentType,
225
226
  contentSize: body.length,
227
+ contentSha256: createHash("sha256").update(body).digest("hex"),
226
228
  nextStep: "After commit confirms, call inscribe_reveal with the same contentType, contentBase64, " +
227
- "plus commitTxid and revealAmount from this response.",
229
+ "plus commitTxid and revealAmount from this response. Pass contentSha256 back to inscribe_reveal " +
230
+ "to have the server abort before the reveal fee is spent if the decoded body differs by even one byte.",
228
231
  });
229
232
  }
230
233
  catch (error) {
@@ -252,12 +255,20 @@ export function registerOrdinalsTools(server) {
252
255
  contentBase64: z
253
256
  .string()
254
257
  .describe("Content as base64-encoded string (must match the commit step)"),
258
+ contentSha256: z
259
+ .string()
260
+ .regex(/^[0-9a-f]{64}$/i)
261
+ .optional()
262
+ .describe("Optional: sha256 hex digest of the decoded body from the inscribe commit response. " +
263
+ "If provided, the server computes sha256 over the locally-decoded body and aborts before " +
264
+ "broadcasting the reveal if they differ — prevents burning the reveal fee on a witness-hash " +
265
+ "mismatch when the base64 param drifts between the inscribe and inscribe_reveal calls."),
255
266
  feeRate: z
256
267
  .union([z.enum(["fast", "medium", "slow"]), z.number().positive()])
257
268
  .optional()
258
269
  .describe("Fee rate for reveal tx (default: medium)"),
259
270
  },
260
- }, async ({ commitTxid, revealAmount, contentType, contentBase64, feeRate }) => {
271
+ }, async ({ commitTxid, revealAmount, contentType, contentBase64, contentSha256, feeRate }) => {
261
272
  try {
262
273
  // Check wallet session
263
274
  const walletManager = getWalletManager();
@@ -277,6 +288,21 @@ export function registerOrdinalsTools(server) {
277
288
  const mempoolApi = new MempoolApi(NETWORK);
278
289
  // Reconstruct the inscription and reveal script
279
290
  const body = Buffer.from(contentBase64, "base64");
291
+ // If the caller supplied the sha256 digest from the commit response, verify the
292
+ // decoded body matches BEFORE we spend the reveal fee. Silent byte drift between
293
+ // the two calls would otherwise commit a tapscript hash that Bitcoin Core rejects
294
+ // as witness-hash-mismatch, burning the reveal-address sats permanently.
295
+ if (contentSha256) {
296
+ const localSha256 = createHash("sha256").update(body).digest("hex");
297
+ if (localSha256.toLowerCase() !== contentSha256.toLowerCase()) {
298
+ return createErrorResponse(new Error(`contentSha256 mismatch: the decoded body's sha256 (${localSha256}) does not match ` +
299
+ `the digest from the inscribe commit response (${contentSha256}). This means the ` +
300
+ `base64 payload passed to inscribe_reveal is not the exact bytes committed on-chain. ` +
301
+ `Aborting BEFORE broadcasting the reveal so the reveal-address sats are not burned. ` +
302
+ `Re-send the exact base64 from the successful inscribe call (or fix the transport / ` +
303
+ `serialization layer that mutated it).`));
304
+ }
305
+ }
280
306
  const inscription = {
281
307
  contentType,
282
308
  body,
@@ -3,7 +3,7 @@ import { hexToBytes } from "@stacks/common";
3
3
  import { getStacksNetwork, getApiBaseUrl } from "../config/networks.js";
4
4
  import { getHiroApi } from "../services/hiro-api.js";
5
5
  import { resolveDefaultFee } from "../utils/fee.js";
6
- import { getSpendLimiter } from "../services/spend-limiter.js";
6
+ import { getSpendLimiter, totalBoundedSpends } from "../services/spend-limiter.js";
7
7
  import { getTrackedNonce, recordNonceUsed, resetTrackedNonce, reconcileWithChain, } from "../services/nonce-tracker.js";
8
8
  // ---------------------------------------------------------------------------
9
9
  // Pending nonce tracking (unified via SharedNonceTracker, issue #413)
@@ -117,6 +117,16 @@ export async function transferStx(account, recipient, amount, memo, fee) {
117
117
  * Call a smart contract function
118
118
  */
119
119
  export async function callContract(account, options) {
120
+ // Safety rail. Swaps, lending and pot entries all funnel through here, and
121
+ // their amounts are denominated in whatever token the call moves — there is no
122
+ // ledger to bill 1,000 ALEX against. The post conditions are the way in: they
123
+ // already bound, in native units, how much of each asset may leave this
124
+ // address. Meter the ones this rail has a ledger for and ignore the rest.
125
+ // Deliberately before makeContractCall, so an over-cap call never signs.
126
+ const meteredSpends = totalBoundedSpends(options.postConditions, account.address);
127
+ for (const spend of meteredSpends) {
128
+ await getSpendLimiter().check(spend.unit, spend.amount, account.address);
129
+ }
120
130
  const networkName = getStacksNetwork(account.network);
121
131
  const nonce = await getNextNonce(account.address, account.network);
122
132
  // Always resolve a clamped fee — prevents @stacks/transactions from over-estimating.
@@ -141,6 +151,9 @@ export async function callContract(account, options) {
141
151
  throw new Error(`Broadcast failed: ${broadcastResponse.error} - ${broadcastResponse.reason}`);
142
152
  }
143
153
  advancePendingNonce(account.address, nonce, broadcastResponse.txid);
154
+ for (const spend of meteredSpends) {
155
+ await getSpendLimiter().record(spend.unit, spend.amount, account.address);
156
+ }
144
157
  return {
145
158
  txid: broadcastResponse.txid,
146
159
  rawTx: transaction.serialize(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aibtc/mcp-server",
3
- "version": "1.67.0",
3
+ "version": "1.69.0",
4
4
  "description": "Bitcoin-native MCP server for AI agents: BTC/STX wallets, DeFi yield, sBTC peg, NFTs, and x402 payments.",
5
5
  "mcpName": "io.github.aibtcdev/mcp-server",
6
6
  "type": "module",
package/skill/SKILL.md CHANGED
@@ -4,7 +4,7 @@ description: Bitcoin L1 wallet for agents - check balances, send BTC, manage UTX
4
4
  license: MIT
5
5
  metadata:
6
6
  author: aibtcdev
7
- version: 1.67.0 # x-release-please-version
7
+ version: 1.69.0 # x-release-please-version
8
8
  npm: "@aibtc/mcp-server"
9
9
  github: https://github.com/aibtcdev/aibtc-mcp-server
10
10
  ---