@aibtc/mcp-server 1.70.1 → 1.71.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.
@@ -1,107 +1,415 @@
1
1
  import { z } from "zod";
2
2
  import { getAccount, getWalletAddress, NETWORK } from "../services/x402.service.js";
3
- import { getStackingService } from "../services/stacking.service.js";
3
+ import { MAX_STAKE_CYCLES, buildPayoutCalldata, getStackingService, } from "../services/stacking.service.js";
4
4
  import { getExplorerTxUrl } from "../config/networks.js";
5
5
  import { createJsonResponse, createErrorResponse } from "../utils/index.js";
6
+ import { btcAddressToPoxAddr } from "../utils/bitcoin.js";
7
+ const POX5_NOTE = "PoX-5: every stake names a signer manager contract; rewards are paid in sBTC through that manager.";
8
+ function stx(ustx) {
9
+ const whole = ustx / 1000000n;
10
+ const frac = (ustx % 1000000n).toString().padStart(6, "0");
11
+ return `${whole}.${frac} STX`;
12
+ }
13
+ function poxSummary(pox) {
14
+ return {
15
+ contract: pox.contractId,
16
+ burnHeight: pox.burnHeight,
17
+ rewardCycle: pox.rewardCycle,
18
+ nextCycleStartHeight: pox.nextCycleStartHeight,
19
+ preparePhaseStartHeight: pox.preparePhaseStartHeight,
20
+ inPreparePhase: pox.inPreparePhase,
21
+ };
22
+ }
23
+ function stakerSummary(info) {
24
+ return {
25
+ signerManager: info.signerManager,
26
+ amountUstx: info.amountUstx.toString(),
27
+ amount: stx(info.amountUstx),
28
+ firstRewardCycle: info.firstRewardCycle,
29
+ numCycles: info.numCycles,
30
+ unlockCycle: info.unlockCycle,
31
+ unlockBurnHeight: info.unlockBurnHeight,
32
+ };
33
+ }
34
+ const payoutInputs = {
35
+ btcRewardAddress: z
36
+ .string()
37
+ .optional()
38
+ .describe("Optional Bitcoin address to receive rewards. Encoded as `{ pox-addr, max-fee }` signer calldata, " +
39
+ "the shape reference signer managers (e.g. Xverse, Fast Pool) use to pay rewards to BTC via an " +
40
+ "sBTC withdrawal. Some managers require it (PlanBetter), some ignore it (native-pool). " +
41
+ "Omit to receive sBTC where the manager supports that."),
42
+ maxWithdrawalFeeSats: z
43
+ .number()
44
+ .int()
45
+ .nonnegative()
46
+ .optional()
47
+ .describe("Max sBTC withdrawal fee (sats) per payout when btcRewardAddress is set. Default 3000."),
48
+ signerCalldataHex: z
49
+ .string()
50
+ .optional()
51
+ .describe("Advanced: raw signer calldata (hex, max 500 bytes) for managers with a custom format. " +
52
+ "Cannot be combined with btcRewardAddress."),
53
+ };
54
+ function resolveCalldata(input) {
55
+ if (input.btcRewardAddress && input.signerCalldataHex) {
56
+ throw new Error("Pass either btcRewardAddress or signerCalldataHex, not both.");
57
+ }
58
+ if (input.btcRewardAddress) {
59
+ return buildPayoutCalldata(btcAddressToPoxAddr(input.btcRewardAddress, NETWORK), BigInt(input.maxWithdrawalFeeSats ?? 3000));
60
+ }
61
+ if (input.signerCalldataHex) {
62
+ const hex = input.signerCalldataHex.replace(/^0x/, "");
63
+ if (!/^([0-9a-fA-F]{2})*$/.test(hex)) {
64
+ throw new Error("signerCalldataHex must be an even-length hex string.");
65
+ }
66
+ return Uint8Array.from(Buffer.from(hex, "hex"));
67
+ }
68
+ return undefined;
69
+ }
6
70
  export function registerStackingTools(server) {
7
- // Get PoX info
71
+ // --- get_pox_info ---
8
72
  server.registerTool("get_pox_info", {
9
- description: "Get current Proof of Transfer (PoX) cycle information.",
73
+ description: "Current PoX-5 state: reward cycle, burn height, next cycle start, and whether the prepare " +
74
+ "phase is active (stake, extend and unstake are refused during it).",
10
75
  }, async () => {
11
76
  try {
12
- const stackingService = getStackingService(NETWORK);
13
- const poxInfo = await stackingService.getPoxInfo();
77
+ const pox = await getStackingService(NETWORK).getPoxState();
14
78
  return createJsonResponse({
15
79
  network: NETWORK,
16
- currentCycle: poxInfo.current_cycle,
17
- nextCycle: poxInfo.next_cycle,
18
- minAmountUstx: poxInfo.min_amount_ustx,
19
- rewardCycleLength: poxInfo.reward_cycle_length,
20
- prepareCycleLength: poxInfo.prepare_cycle_length,
21
- currentBurnchainBlockHeight: poxInfo.current_burnchain_block_height,
22
- totalLiquidSupplyUstx: poxInfo.total_liquid_supply_ustx,
80
+ ...poxSummary(pox),
81
+ firstBurnHeight: pox.firstBurnHeight,
82
+ rewardCycleLength: pox.rewardCycleLength,
83
+ prepareCycleLength: pox.prepareCycleLength,
84
+ signerSetMinUstx: pox.signerSetMinUstx.toString(),
85
+ totalLiquidSupplyUstx: pox.totalLiquidSupplyUstx.toString(),
86
+ note: POX5_NOTE,
23
87
  });
24
88
  }
25
89
  catch (error) {
26
90
  return createErrorResponse(error);
27
91
  }
28
92
  });
29
- // Get stacking status
93
+ // --- get_stacking_status ---
30
94
  server.registerTool("get_stacking_status", {
31
- description: "Check if an address is currently stacking STX.",
95
+ description: "PoX-5 staking status for an address: locked amount, signer manager, lock period, unlock " +
96
+ "cycle and burn height, any protocol bond membership, and locked/unlocked STX balance.",
32
97
  inputSchema: {
33
98
  address: z
34
99
  .string()
35
100
  .optional()
36
- .describe("Wallet address to check. Uses configured wallet if not provided."),
101
+ .describe("Stacks address to check. Uses the active wallet if omitted."),
37
102
  },
38
103
  }, async ({ address }) => {
39
104
  try {
40
- const stackingService = getStackingService(NETWORK);
41
105
  const walletAddress = address || (await getWalletAddress());
42
- const status = await stackingService.getStackingStatus(walletAddress);
106
+ const status = await getStackingService(NETWORK).getStakingStatus(walletAddress);
43
107
  return createJsonResponse({
44
108
  address: walletAddress,
45
109
  network: NETWORK,
46
- ...status,
110
+ staking: status.staking !== null,
111
+ stake: status.staking ? stakerSummary(status.staking) : null,
112
+ bond: status.bond
113
+ ? {
114
+ bondIndex: status.bond.bondIndex,
115
+ signerManager: status.bond.signerManager,
116
+ amountUstx: status.bond.amountUstx.toString(),
117
+ amountSats: status.bond.amountSats.toString(),
118
+ isL1Lock: status.bond.isL1Lock,
119
+ }
120
+ : null,
121
+ balance: {
122
+ totalUstx: status.account.balanceUstx.toString(),
123
+ lockedUstx: status.account.lockedUstx.toString(),
124
+ unlockedUstx: status.account.unlockedUstx.toString(),
125
+ burnchainUnlockHeight: status.account.burnchainUnlockHeight,
126
+ },
127
+ pox: poxSummary(status.pox),
47
128
  });
48
129
  }
49
130
  catch (error) {
50
131
  return createErrorResponse(error);
51
132
  }
52
133
  });
53
- // Stack STX
134
+ // --- list_stacking_signers ---
135
+ server.registerTool("list_stacking_signers", {
136
+ description: "List PoX-5 signer managers in the signer set for a reward cycle (default: next cycle), with " +
137
+ "the STX delegated to each. A signer manager is what stack_stx stakes with. Only signers with " +
138
+ "at least the signer-set minimum delegated appear here; other registered managers can still be " +
139
+ "staked with by contract id. Set withPayoutInfo to see how each manager pays stakers (slower).",
140
+ inputSchema: {
141
+ rewardCycle: z
142
+ .number()
143
+ .int()
144
+ .positive()
145
+ .optional()
146
+ .describe("Reward cycle to list. Defaults to the next cycle (the one a new stake joins)."),
147
+ withPayoutInfo: z
148
+ .boolean()
149
+ .optional()
150
+ .default(false)
151
+ .describe("Also read each manager's claim path (one extra request per signer)."),
152
+ },
153
+ }, async ({ rewardCycle, withPayoutInfo }) => {
154
+ try {
155
+ const service = getStackingService(NETWORK);
156
+ const pox = await service.getPoxState();
157
+ const cycle = rewardCycle ?? pox.rewardCycle + 1;
158
+ const signers = (await service.getSignerSet(cycle)).sort((a, b) => b.delegatedUstx > a.delegatedUstx ? 1 : b.delegatedUstx < a.delegatedUstx ? -1 : 0);
159
+ const rows = [];
160
+ for (const s of signers) {
161
+ rows.push({
162
+ signerManager: s.signerManager,
163
+ delegatedUstx: s.delegatedUstx.toString(),
164
+ delegated: stx(s.delegatedUstx),
165
+ ...(withPayoutInfo && { stakerClaim: await service.getClaimStyle(s.signerManager) }),
166
+ });
167
+ }
168
+ return createJsonResponse({
169
+ network: NETWORK,
170
+ rewardCycle: cycle,
171
+ count: rows.length,
172
+ signers: rows,
173
+ ...(withPayoutInfo && {
174
+ stakerClaimKey: {
175
+ "staker-arg": "on-chain; claim_stacking_rewards works",
176
+ caller: "on-chain; claim_stacking_rewards works (paid to the caller)",
177
+ none: "no on-chain staker claim; the manager pays off-chain or not at all",
178
+ },
179
+ }),
180
+ note: "Managers can restrict who may stake (allowlists, minimums, required payout calldata). " +
181
+ "Check a manager's own terms before staking; a refused stake aborts on chain.",
182
+ });
183
+ }
184
+ catch (error) {
185
+ return createErrorResponse(error);
186
+ }
187
+ });
188
+ // --- stack_stx ---
54
189
  server.registerTool("stack_stx", {
55
- description: `Lock STX for stacking to earn BTC rewards.
190
+ description: `Lock STX in PoX-5 with a signer manager to earn sBTC rewards.
56
191
 
57
- Requires a Bitcoin address (hash) for receiving rewards.`,
192
+ The lock starts next reward cycle and lasts numCycles cycles (1-${MAX_STAKE_CYCLES}). Pick a signer manager with list_stacking_signers. Refused during the prepare phase and if the address is already staking (use extend_stacking). Locking is not a transfer: the transaction carries a staking post-condition for exactly the locked amount.`,
58
193
  inputSchema: {
59
- amount: z.string().describe("Amount of STX to stack (in micro-STX)"),
60
- poxAddressVersion: z.number().describe("Bitcoin address version (0 for P2PKH, 1 for P2SH, etc.)"),
61
- poxAddressHashbytes: z.string().describe("Bitcoin address hash (hex string)"),
62
- startBurnHeight: z.number().describe("Bitcoin block height to start stacking"),
63
- lockPeriod: z.number().min(1).max(12).describe("Number of reward cycles to lock (1-12)"),
194
+ signerManager: z
195
+ .string()
196
+ .describe("Signer manager contract id, e.g. SP8HK160YD5GHXP69VGA0TC7AQJ1X4CDW3XVERSE.xverse-signer-manager-2"),
197
+ amount: z.string().describe("Amount to lock, in micro-STX"),
198
+ numCycles: z
199
+ .number()
200
+ .int()
201
+ .min(1)
202
+ .max(MAX_STAKE_CYCLES)
203
+ .describe(`Reward cycles to lock (1-${MAX_STAKE_CYCLES}; one cycle is about two weeks)`),
204
+ ...payoutInputs,
64
205
  },
65
- }, async ({ amount, poxAddressVersion, poxAddressHashbytes, startBurnHeight, lockPeriod }) => {
206
+ }, async ({ signerManager, amount, numCycles, ...payout }) => {
66
207
  try {
67
- const stackingService = getStackingService(NETWORK);
68
208
  const account = await getAccount();
69
- const result = await stackingService.stack(account, BigInt(amount), { version: poxAddressVersion, hashbytes: poxAddressHashbytes }, startBurnHeight, lockPeriod);
209
+ const result = await getStackingService(NETWORK).stake(account, {
210
+ signerManager,
211
+ amountUstx: BigInt(amount),
212
+ numCycles,
213
+ signerCalldata: resolveCalldata(payout),
214
+ });
70
215
  return createJsonResponse({
71
216
  success: true,
72
217
  txid: result.txid,
73
- stacker: account.address,
74
- amount,
75
- lockPeriod,
76
- startBurnHeight,
77
- network: NETWORK,
78
218
  explorerUrl: getExplorerTxUrl(result.txid, NETWORK),
219
+ staker: account.address,
220
+ signerManager,
221
+ amountUstx: amount,
222
+ amount: stx(BigInt(amount)),
223
+ firstRewardCycle: result.pox.rewardCycle + 1,
224
+ numCycles,
225
+ unlockCycle: result.unlockCycle,
226
+ unlockBurnHeight: result.unlockBurnHeight,
227
+ rewardPayout: payout.btcRewardAddress
228
+ ? `BTC to ${payout.btcRewardAddress} (if the manager supports it)`
229
+ : payout.signerCalldataHex
230
+ ? "custom signer calldata"
231
+ : "sBTC (if the manager supports it)",
232
+ network: NETWORK,
79
233
  });
80
234
  }
81
235
  catch (error) {
82
236
  return createErrorResponse(error);
83
237
  }
84
238
  });
85
- // Extend stacking
239
+ // --- extend_stacking ---
86
240
  server.registerTool("extend_stacking", {
87
- description: "Extend an existing stacking lock period.",
241
+ description: `Update an existing PoX-5 stake: extend the lock, add STX, switch signer manager, or change payout calldata (pox-5 stake-update).
242
+
243
+ Any combination in one call. Increasing locks more of the address's unlocked STX. Refused during the prepare phase. The lock may not run more than ${MAX_STAKE_CYCLES} cycles past the next cycle.`,
88
244
  inputSchema: {
89
- extendCount: z.number().min(1).max(12).describe("Number of additional cycles to lock"),
90
- poxAddressVersion: z.number().describe("Bitcoin address version"),
91
- poxAddressHashbytes: z.string().describe("Bitcoin address hash (hex string)"),
245
+ cyclesToExtend: z
246
+ .number()
247
+ .int()
248
+ .nonnegative()
249
+ .optional()
250
+ .default(0)
251
+ .describe("Cycles to add to the current unlock cycle"),
252
+ amountIncrease: z
253
+ .string()
254
+ .optional()
255
+ .default("0")
256
+ .describe("Additional micro-STX to lock"),
257
+ signerManager: z
258
+ .string()
259
+ .optional()
260
+ .describe("New signer manager contract id. Defaults to the current one."),
261
+ ...payoutInputs,
92
262
  },
93
- }, async ({ extendCount, poxAddressVersion, poxAddressHashbytes }) => {
263
+ }, async ({ cyclesToExtend, amountIncrease, signerManager, ...payout }) => {
94
264
  try {
95
- const stackingService = getStackingService(NETWORK);
96
265
  const account = await getAccount();
97
- const result = await stackingService.extendStacking(account, extendCount, { version: poxAddressVersion, hashbytes: poxAddressHashbytes });
266
+ const result = await getStackingService(NETWORK).updateStake(account, {
267
+ signerManager,
268
+ cyclesToExtend,
269
+ amountIncreaseUstx: BigInt(amountIncrease),
270
+ signerCalldata: resolveCalldata(payout),
271
+ });
98
272
  return createJsonResponse({
99
273
  success: true,
100
274
  txid: result.txid,
101
- stacker: account.address,
102
- extendCount,
275
+ explorerUrl: getExplorerTxUrl(result.txid, NETWORK),
276
+ staker: account.address,
277
+ previous: stakerSummary(result.previous),
278
+ signerManager: result.signerManager,
279
+ newAmountUstx: result.newAmountUstx.toString(),
280
+ newAmount: stx(result.newAmountUstx),
281
+ unlockCycle: result.unlockCycle,
282
+ unlockBurnHeight: result.unlockBurnHeight,
103
283
  network: NETWORK,
284
+ });
285
+ }
286
+ catch (error) {
287
+ return createErrorResponse(error);
288
+ }
289
+ });
290
+ // --- unstake_stx ---
291
+ server.registerTool("unstake_stx", {
292
+ description: "Stop a PoX-5 stake early (pox-5 unstake). The STX stays locked through the current reward " +
293
+ "cycle and unlocks at the start of the next one. Refused during the prepare phase.",
294
+ inputSchema: {},
295
+ }, async () => {
296
+ try {
297
+ const account = await getAccount();
298
+ const result = await getStackingService(NETWORK).unstake(account);
299
+ return createJsonResponse({
300
+ success: true,
301
+ txid: result.txid,
104
302
  explorerUrl: getExplorerTxUrl(result.txid, NETWORK),
303
+ staker: account.address,
304
+ previous: stakerSummary(result.previous),
305
+ unlockCycle: result.unlockCycle,
306
+ unlockBurnHeight: result.unlockBurnHeight,
307
+ network: NETWORK,
308
+ });
309
+ }
310
+ catch (error) {
311
+ return createErrorResponse(error);
312
+ }
313
+ });
314
+ // --- get_stacking_rewards ---
315
+ server.registerTool("get_stacking_rewards", {
316
+ description: "PoX-5 sBTC rewards an address has earned from its signer manager for one reward cycle and not " +
317
+ "yet claimed (before the manager's fees), whether the manager still has to pull them from pox-5, " +
318
+ "and whether the manager supports an on-chain staker claim.",
319
+ inputSchema: {
320
+ rewardCycle: z.number().int().nonnegative().describe("Reward cycle to check"),
321
+ address: z
322
+ .string()
323
+ .optional()
324
+ .describe("Stacks address. Uses the active wallet if omitted."),
325
+ signerManager: z
326
+ .string()
327
+ .optional()
328
+ .describe("Signer manager to check. Defaults to the address's current signer manager."),
329
+ },
330
+ }, async ({ rewardCycle, address, signerManager }) => {
331
+ try {
332
+ const service = getStackingService(NETWORK);
333
+ const staker = address || (await getWalletAddress());
334
+ const manager = signerManager ?? (await service.getStakerInfo(staker))?.signerManager;
335
+ if (!manager) {
336
+ throw new Error(`${staker} is not currently staking; pass signerManager to check rewards from a past signer.`);
337
+ }
338
+ const [earnedSats, unpulledSats, claimStyle] = await Promise.all([
339
+ service.getStakerUnclaimedRewards(manager, rewardCycle, staker),
340
+ service.getSignerUnpulledRewards(manager, rewardCycle),
341
+ service.getClaimStyle(manager),
342
+ ]);
343
+ return createJsonResponse({
344
+ address: staker,
345
+ network: NETWORK,
346
+ rewardCycle,
347
+ signerManager: manager,
348
+ unclaimedSatsBeforeFees: earnedSats.toString(),
349
+ managerUnpulledSats: unpulledSats.toString(),
350
+ stakerClaim: claimStyle,
351
+ next: earnedSats === 0n
352
+ ? "Nothing to claim for this cycle."
353
+ : claimStyle === "none"
354
+ ? "This manager has no on-chain staker claim; it pays stakers off-chain."
355
+ : "Call claim_stacking_rewards with this rewardCycle.",
356
+ });
357
+ }
358
+ catch (error) {
359
+ return createErrorResponse(error);
360
+ }
361
+ });
362
+ // --- claim_stacking_rewards ---
363
+ server.registerTool("claim_stacking_rewards", {
364
+ description: "Claim PoX-5 sBTC rewards for one reward cycle through the staker's signer manager. If the " +
365
+ "manager has not yet pulled that cycle's rewards from pox-5, this first sends the manager's " +
366
+ "permissionless claim-rewards (a second transaction, paid by this wallet), then the staker " +
367
+ "claim. Rewards arrive as sBTC, or as a BTC withdrawal if a payout address was set when " +
368
+ "staking. Not available for managers that pay off-chain.",
369
+ inputSchema: {
370
+ rewardCycle: z.number().int().nonnegative().describe("Reward cycle to claim"),
371
+ signerManager: z
372
+ .string()
373
+ .optional()
374
+ .describe("Signer manager to claim from. Defaults to the wallet's current signer manager."),
375
+ },
376
+ }, async ({ rewardCycle, signerManager }) => {
377
+ try {
378
+ const service = getStackingService(NETWORK);
379
+ const account = await getAccount();
380
+ const manager = signerManager ?? (await service.getStakerInfo(account.address))?.signerManager;
381
+ if (!manager) {
382
+ throw new Error(`${account.address} is not currently staking; pass signerManager to claim from a past signer.`);
383
+ }
384
+ const [earnedSats, unpulledSats, claimStyle] = await Promise.all([
385
+ service.getStakerUnclaimedRewards(manager, rewardCycle, account.address),
386
+ service.getSignerUnpulledRewards(manager, rewardCycle),
387
+ service.getClaimStyle(manager),
388
+ ]);
389
+ if (claimStyle === "none") {
390
+ throw new Error(`${manager} has no on-chain staker claim (claim-staker-rewards); it pays stakers off-chain.`);
391
+ }
392
+ if (earnedSats === 0n) {
393
+ throw new Error(`No unclaimed rewards for ${account.address} from ${manager} in cycle ${rewardCycle}.`);
394
+ }
395
+ let pull = null;
396
+ if (unpulledSats > 0n) {
397
+ const pulled = await service.pullSignerRewards(account, manager, rewardCycle, unpulledSats);
398
+ pull = { txid: pulled.txid, explorerUrl: getExplorerTxUrl(pulled.txid, NETWORK) };
399
+ }
400
+ const claim = await service.claimStakerRewards(account, manager, rewardCycle, claimStyle);
401
+ return createJsonResponse({
402
+ success: true,
403
+ network: NETWORK,
404
+ rewardCycle,
405
+ signerManager: manager,
406
+ unclaimedSatsBeforeFees: earnedSats.toString(),
407
+ managerPull: pull,
408
+ txid: claim.txid,
409
+ explorerUrl: getExplorerTxUrl(claim.txid, NETWORK),
410
+ note: pull
411
+ ? "Two transactions were sent in nonce order: the manager's pull, then the staker claim."
412
+ : undefined,
105
413
  });
106
414
  }
107
415
  catch (error) {
@@ -17,6 +17,7 @@ import { contractPrincipalCV, standardPrincipalCV, uintCV, hexToCV, cvToValue, }
17
17
  import { NETWORK } from "../services/x402.service.js";
18
18
  import { getWalletAddress } from "../services/x402.service.js";
19
19
  import { getHiroApi } from "../services/hiro-api.js";
20
+ import { getStackingService } from "../services/stacking.service.js";
20
21
  import { createJsonResponse, createErrorResponse } from "../utils/index.js";
21
22
  // ============================================================================
22
23
  // Constants
@@ -41,10 +42,6 @@ const ALEX_TOKEN_Y_NAME = "token-abtc";
41
42
  const ALEX_FACTOR = 100_000_000;
42
43
  // Bitflow public API
43
44
  const BITFLOW_API = "https://app.bitflow.finance/api";
44
- // Stacking (PoX-4)
45
- const POX_CONTRACT = "SP000000000000000000002Q6VF78";
46
- const POX_NAME = "pox-4";
47
- const POX_CONTRACT_ID = `${POX_CONTRACT}.${POX_NAME}`;
48
45
  // Mainnet Hiro API base URL (direct — not network-switched, this skill is mainnet-only)
49
46
  const MAINNET_HIRO_API = "https://api.hiro.so";
50
47
  // ============================================================================
@@ -225,24 +222,18 @@ async function readStackingPosition(walletAddress) {
225
222
  details: {},
226
223
  };
227
224
  try {
228
- const hiro = getHiroApi("mainnet");
229
- const res = await hiro.callReadOnlyFunction(POX_CONTRACT_ID, "get-stacker-info", [standardPrincipalCV(walletAddress)], POX_CONTRACT);
230
- if (res.okay && res.result) {
231
- const hex = res.result.startsWith("0x") ? res.result.slice(2) : res.result;
232
- const cv = hexToCV(hex);
233
- const val = cvToValue(cv, true);
234
- if (val && typeof val === "object" && "lock-amount" in val) {
235
- const lockAmount = val["lock-amount"];
236
- pos.valueSats =
237
- typeof lockAmount === "bigint"
238
- ? Number(lockAmount)
239
- : typeof lockAmount === "number"
240
- ? lockAmount
241
- : 0;
242
- pos.apyPct = 8.0;
243
- pos.details.apySource = "static estimate, not live";
244
- pos.details.stackerInfo = val;
245
- }
225
+ const info = await getStackingService("mainnet").getStakerInfo(walletAddress);
226
+ if (info) {
227
+ pos.valueSats = Number(info.amountUstx);
228
+ pos.apyPct = 8.0;
229
+ pos.details.apySource = "static estimate, not live";
230
+ pos.details.stakerInfo = {
231
+ signerManager: info.signerManager,
232
+ firstRewardCycle: info.firstRewardCycle,
233
+ numCycles: info.numCycles,
234
+ unlockCycle: info.unlockCycle,
235
+ unlockBurnHeight: info.unlockBurnHeight,
236
+ };
246
237
  }
247
238
  }
248
239
  catch (e) {
@@ -231,4 +231,12 @@ export declare function deriveTaprootKeyPair(mnemonic: string, network: Network)
231
231
  * ```
232
232
  */
233
233
  export declare function deriveNostrKeyPair(mnemonic: string, _network: Network): NostrKeyPair;
234
+ /**
235
+ * Decode a Bitcoin address into the `{ version, hashbytes }` pox-addr tuple used by
236
+ * Stacks contracts (sBTC withdrawals, PoX signer-manager payout addresses).
237
+ */
238
+ export declare function btcAddressToPoxAddr(address: string, network: Network): {
239
+ version: number;
240
+ hashbytesHex: string;
241
+ };
234
242
  //# sourceMappingURL=bitcoin.d.ts.map
@@ -326,4 +326,28 @@ export function deriveNostrKeyPair(mnemonic, _network) {
326
326
  publicKey,
327
327
  };
328
328
  }
329
+ /**
330
+ * Decode a Bitcoin address into the `{ version, hashbytes }` pox-addr tuple used by
331
+ * Stacks contracts (sBTC withdrawals, PoX signer-manager payout addresses).
332
+ */
333
+ export function btcAddressToPoxAddr(address, network) {
334
+ const decoded = btc
335
+ .Address(network === "mainnet" ? btc.NETWORK : btc.TEST_NETWORK)
336
+ .decode(address);
337
+ if (decoded.type === "tr" && decoded.pubkey) {
338
+ return { version: 0x06, hashbytesHex: Buffer.from(decoded.pubkey).toString("hex") };
339
+ }
340
+ switch (decoded.type) {
341
+ case "pkh":
342
+ return { version: 0x00, hashbytesHex: Buffer.from(decoded.hash).toString("hex") };
343
+ case "sh":
344
+ return { version: 0x01, hashbytesHex: Buffer.from(decoded.hash).toString("hex") };
345
+ case "wpkh":
346
+ return { version: 0x04, hashbytesHex: Buffer.from(decoded.hash).toString("hex") };
347
+ case "wsh":
348
+ return { version: 0x05, hashbytesHex: Buffer.from(decoded.hash).toString("hex") };
349
+ default:
350
+ throw new Error("Unsupported BTC address type. Supported: P2PKH, P2SH, P2WPKH, P2WSH, P2TR.");
351
+ }
352
+ }
329
353
  //# sourceMappingURL=bitcoin.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aibtc/mcp-server",
3
- "version": "1.70.1",
3
+ "version": "1.71.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",
@@ -59,10 +59,10 @@
59
59
  "@scure/bip32": "^1.4.0",
60
60
  "@scure/bip39": "^2.0.1",
61
61
  "@scure/btc-signer": "^2.0.1",
62
- "@stacks/common": "^7.3.1",
63
- "@stacks/encryption": "^7.3.1",
64
- "@stacks/network": "^7.3.1",
65
- "@stacks/transactions": "^7.3.1",
62
+ "@stacks/common": "^7.6.0",
63
+ "@stacks/encryption": "^7.6.0",
64
+ "@stacks/network": "^7.6.0",
65
+ "@stacks/transactions": "^7.6.0",
66
66
  "@stacks/wallet-sdk": "^7.2.0",
67
67
  "alex-sdk": "^3.2.1",
68
68
  "axios": "^1.17.0",
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.70.1 # x-release-please-version
7
+ version: 1.71.0 # x-release-please-version
8
8
  npm: "@aibtc/mcp-server"
9
9
  github: https://github.com/aibtcdev/aibtc-mcp-server
10
10
  ---
@@ -360,7 +360,7 @@ Utility services for agents:
360
360
  | Category | Endpoints |
361
361
  |----------|-----------|
362
362
  | **Inference** | `/inference/openrouter/chat`, `/inference/cloudflare/chat` |
363
- | **Stacks** | `/stacks/address`, `/stacks/decode`, `/stacks/profile` |
363
+ | **Stacks** | `/stacks/address/{address}`, `/stacks/decode/transaction`, `/stacks/decode/clarity`, `/stacks/profile/{address}` |
364
364
  | **Hashing** | `/hashing/sha256`, `/hashing/keccak256`, `/hashing/hash160` |
365
365
  | **Storage** | `/storage/kv/*`, `/storage/paste/*`, `/storage/db/*`, `/storage/memory/*` |
366
366