@aibtc/mcp-server 1.70.0 → 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.
@@ -88,6 +88,21 @@ function formatSigAuthForApi(sigAuth) {
88
88
  : "0x" + sigAuth.pubkey,
89
89
  };
90
90
  }
91
+ /**
92
+ * The Pillar smart wallet's Fast Pool functions are hardcoded to pox-4
93
+ * (`pox-4.allow-contract-caller` + `pox4-fast-pool-v3.delegate-stx`, and
94
+ * `pox-4.revoke-delegate-stx`). Once PoX has moved past pox-4 those calls abort
95
+ * on chain, so refuse before signing instead of burning gas on a sure failure.
96
+ */
97
+ async function assertFastPoolPoxActive(action) {
98
+ const pox = await getHiroApi(NETWORK).getPoxInfo();
99
+ if (!pox.contract_id.endsWith(".pox-4")) {
100
+ throw new Error(`${action} is unavailable: the Pillar smart wallet's Fast Pool path calls pox-4, but the ` +
101
+ `active PoX contract is ${pox.contract_id}, so the transaction would abort on chain. ` +
102
+ `This needs a Pillar wallet contract update. For STX staking on PoX-5, use stack_stx ` +
103
+ `with a signer manager (see list_stacking_signers).`);
104
+ }
105
+ }
91
106
  // ============================================================================
92
107
  // Tool Registration
93
108
  // ============================================================================
@@ -1041,7 +1056,8 @@ export function registerPillarDirectTools(server) {
1041
1056
  server.registerTool("pillar_direct_stack_stx", {
1042
1057
  description: "Stack STX from your Pillar smart wallet via Fast Pool or Stacking DAO. " +
1043
1058
  "Agent-signed, no browser needed. Backend sponsors gas. " +
1044
- "Fast Pool delegates STX to the pox4-fast-pool-v3 contract. " +
1059
+ "Fast Pool delegates STX to the pox4-fast-pool-v3 contract through pox-4, and is refused while " +
1060
+ "pox-4 is not the active PoX contract (PoX-5 is live; use stack_stx for PoX-5 staking). " +
1045
1061
  "Stacking DAO deposits STX into Stacking DAO core for stSTX yield. " +
1046
1062
  "Your wallet must be enrolled in dual stacking first (automatic for v2 wallets with sBTC).",
1047
1063
  inputSchema: {
@@ -1056,6 +1072,9 @@ export function registerPillarDirectTools(server) {
1056
1072
  },
1057
1073
  }, async ({ stxAmount, pool }) => {
1058
1074
  try {
1075
+ if (pool === "fast-pool") {
1076
+ await assertFastPoolPoxActive("Fast Pool stacking");
1077
+ }
1059
1078
  const { keyService, session } = await requireActiveKey();
1060
1079
  const authId = generateAuthId();
1061
1080
  // Build SIP-018 structured data matching the contract's hash builder.
@@ -1099,12 +1118,14 @@ export function registerPillarDirectTools(server) {
1099
1118
  });
1100
1119
  // --- pillar_direct_revoke_fast_pool ---
1101
1120
  server.registerTool("pillar_direct_revoke_fast_pool", {
1102
- description: "Revoke Fast Pool STX delegation from your Pillar smart wallet. " +
1121
+ description: "Revoke Fast Pool STX delegation from your Pillar smart wallet (pox-4 revoke-delegate-stx; " +
1122
+ "refused while pox-4 is not the active PoX contract). " +
1103
1123
  "Agent-signed, no browser needed. Backend sponsors gas. " +
1104
1124
  "After revoking, STX stays locked until the current PoX cycle ends, then returns to liquid.",
1105
1125
  inputSchema: {},
1106
1126
  }, async () => {
1107
1127
  try {
1128
+ await assertFastPoolPoxActive("Revoking Fast Pool");
1108
1129
  const { keyService, session } = await requireActiveKey();
1109
1130
  const authId = generateAuthId();
1110
1131
  const structuredData = tupleCV({
@@ -1,4 +1,3 @@
1
- import * as btc from "@scure/btc-signer";
2
1
  import { z } from "zod";
3
2
  import { getAccount, getWalletAddress, NETWORK } from "../services/x402.service.js";
4
3
  import { getSbtcService } from "../services/sbtc.service.js";
@@ -8,26 +7,8 @@ import { getContracts, parseContractId } from "../config/contracts.js";
8
7
  import { createJsonResponse, createErrorResponse, resolveFee } from "../utils/index.js";
9
8
  import { getWalletManager } from "../services/wallet-manager.js";
10
9
  import { MempoolApi, getMempoolTxUrl } from "../services/mempool-api.js";
11
- import { getBtcNetwork } from "../transactions/bitcoin-builder.js";
12
10
  import { sponsoredSchema } from "./schemas.js";
13
- function parseBtcRecipientTuple(btcRecipientAddress) {
14
- const decoded = btc.Address(getBtcNetwork(NETWORK)).decode(btcRecipientAddress);
15
- if (decoded.type === "tr" && decoded.pubkey) {
16
- return { version: 0x06, hashbytesHex: Buffer.from(decoded.pubkey).toString("hex") };
17
- }
18
- switch (decoded.type) {
19
- case "pkh":
20
- return { version: 0x00, hashbytesHex: Buffer.from(decoded.hash).toString("hex") };
21
- case "sh":
22
- return { version: 0x01, hashbytesHex: Buffer.from(decoded.hash).toString("hex") };
23
- case "wpkh":
24
- return { version: 0x04, hashbytesHex: Buffer.from(decoded.hash).toString("hex") };
25
- case "wsh":
26
- return { version: 0x05, hashbytesHex: Buffer.from(decoded.hash).toString("hex") };
27
- default:
28
- throw new Error("Unsupported BTC recipient address type. Supported: P2PKH, P2SH, P2WPKH, P2WSH, P2TR.");
29
- }
30
- }
11
+ import { btcAddressToPoxAddr } from "../utils/bitcoin.js";
31
12
  function getReadonlySenderAddress() {
32
13
  const contracts = getContracts(NETWORK);
33
14
  return parseContractId(contracts.SBTC_REGISTRY).address;
@@ -107,7 +88,7 @@ Example: To send 0.001 sBTC, use amount "100000" (satoshis).`,
107
88
  const sbtcService = getSbtcService(NETWORK);
108
89
  const account = await getAccount();
109
90
  const resolvedFee = await resolveFee(fee, NETWORK, "contract_call");
110
- const recipientTuple = parseBtcRecipientTuple(btcRecipientAddress);
91
+ const recipientTuple = btcAddressToPoxAddr(btcRecipientAddress, NETWORK);
111
92
  const result = await sbtcService.initiateWithdrawal(account, BigInt(amount), BigInt(maxFee), recipientTuple, resolvedFee, sponsored);
112
93
  let requestId = null;
113
94
  try {
@@ -68,6 +68,10 @@ export const TOOL_SKILL_MAP = {
68
68
  get_stacking_status: "stacking",
69
69
  stack_stx: "stacking",
70
70
  extend_stacking: "stacking",
71
+ list_stacking_signers: "stacking",
72
+ unstake_stx: "stacking",
73
+ get_stacking_rewards: "stacking",
74
+ claim_stacking_rewards: "stacking",
71
75
  // dual-stacking skill — sBTC Dual Stacking yield protocol
72
76
  dual_stacking_status: "dual-stacking",
73
77
  dual_stacking_get_rewards: "dual-stacking",
@@ -225,8 +229,6 @@ export const TOOL_SKILL_MAP = {
225
229
  taproot_verify_cosig: "ordinals-p2p",
226
230
  taproot_multisig_guide: "ordinals-p2p",
227
231
  // identity skill — ERC-8004 on-chain agent identity
228
- register_identity: "identity",
229
- get_identity: "identity",
230
232
  identity_get_last_id: "identity",
231
233
  identity_get: "identity",
232
234
  identity_get_metadata: "identity",
@@ -238,12 +240,7 @@ export const TOOL_SKILL_MAP = {
238
240
  identity_unset_wallet: "identity",
239
241
  identity_transfer: "identity",
240
242
  // reputation skill — ERC-8004 on-chain agent reputation
241
- give_feedback: "reputation",
242
- get_reputation: "reputation",
243
243
  // validation skill — ERC-8004 on-chain agent validation
244
- request_validation: "validation",
245
- get_validation_status: "validation",
246
- get_validation_summary: "validation",
247
244
  // stackspot skill — stacking lottery pots
248
245
  stackspot_list_pots: "stackspot",
249
246
  stackspot_get_pot_state: "stackspot",
@@ -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) {
@@ -7,9 +7,8 @@
7
7
  *
8
8
  * Spec: https://github.com/bitcoin/bips/blob/master/bip-0322.mediawiki
9
9
  *
10
- * These primitives are shared across:
11
- * - src/tools/signing.tools.ts (btc_sign_message / btc_verify_message)
12
- * - src/tools/news.tools.ts (news_file_signal auth headers)
10
+ * Used by src/tools/signing.tools.ts (btc_sign_message / btc_verify_message)
11
+ * and the BIP-322-authenticated aibtc.com tools.
13
12
  */
14
13
  /**
15
14
  * BIP-322 tagged hash: SHA256(SHA256(tag) || SHA256(tag) || msg)