@persistenceone/bridgekitty 0.3.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 (63) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +232 -0
  3. package/dist/backends/across.d.ts +10 -0
  4. package/dist/backends/across.js +285 -0
  5. package/dist/backends/debridge.d.ts +11 -0
  6. package/dist/backends/debridge.js +380 -0
  7. package/dist/backends/lifi.d.ts +19 -0
  8. package/dist/backends/lifi.js +295 -0
  9. package/dist/backends/persistence.d.ts +86 -0
  10. package/dist/backends/persistence.js +642 -0
  11. package/dist/backends/relay.d.ts +11 -0
  12. package/dist/backends/relay.js +292 -0
  13. package/dist/backends/squid.d.ts +31 -0
  14. package/dist/backends/squid.js +476 -0
  15. package/dist/backends/types.d.ts +125 -0
  16. package/dist/backends/types.js +11 -0
  17. package/dist/index.d.ts +2 -0
  18. package/dist/index.js +154 -0
  19. package/dist/routing/engine.d.ts +49 -0
  20. package/dist/routing/engine.js +336 -0
  21. package/dist/tools/check-status.d.ts +3 -0
  22. package/dist/tools/check-status.js +93 -0
  23. package/dist/tools/execute-bridge.d.ts +3 -0
  24. package/dist/tools/execute-bridge.js +428 -0
  25. package/dist/tools/get-chains.d.ts +3 -0
  26. package/dist/tools/get-chains.js +162 -0
  27. package/dist/tools/get-quote.d.ts +3 -0
  28. package/dist/tools/get-quote.js +534 -0
  29. package/dist/tools/get-tokens.d.ts +3 -0
  30. package/dist/tools/get-tokens.js +128 -0
  31. package/dist/tools/help.d.ts +2 -0
  32. package/dist/tools/help.js +204 -0
  33. package/dist/tools/multi-quote.d.ts +3 -0
  34. package/dist/tools/multi-quote.js +310 -0
  35. package/dist/tools/onboard.d.ts +3 -0
  36. package/dist/tools/onboard.js +218 -0
  37. package/dist/tools/wallet.d.ts +14 -0
  38. package/dist/tools/wallet.js +744 -0
  39. package/dist/tools/xprt-farm.d.ts +3 -0
  40. package/dist/tools/xprt-farm.js +1308 -0
  41. package/dist/tools/xprt-rewards.d.ts +2 -0
  42. package/dist/tools/xprt-rewards.js +177 -0
  43. package/dist/tools/xprt-staking.d.ts +2 -0
  44. package/dist/tools/xprt-staking.js +565 -0
  45. package/dist/utils/chains.d.ts +22 -0
  46. package/dist/utils/chains.js +154 -0
  47. package/dist/utils/circuit-breaker.d.ts +64 -0
  48. package/dist/utils/circuit-breaker.js +160 -0
  49. package/dist/utils/evm.d.ts +18 -0
  50. package/dist/utils/evm.js +46 -0
  51. package/dist/utils/fill-detector.d.ts +70 -0
  52. package/dist/utils/fill-detector.js +298 -0
  53. package/dist/utils/gas-estimator.d.ts +67 -0
  54. package/dist/utils/gas-estimator.js +340 -0
  55. package/dist/utils/sanitize-error.d.ts +23 -0
  56. package/dist/utils/sanitize-error.js +101 -0
  57. package/dist/utils/token-registry.d.ts +70 -0
  58. package/dist/utils/token-registry.js +669 -0
  59. package/dist/utils/tokens.d.ts +17 -0
  60. package/dist/utils/tokens.js +37 -0
  61. package/dist/utils/tx-simulator.d.ts +27 -0
  62. package/dist/utils/tx-simulator.js +105 -0
  63. package/package.json +75 -0
@@ -0,0 +1,565 @@
1
+ import { z } from "zod";
2
+ import { getKey } from "./wallet.js";
3
+ import { sanitizeError } from "../utils/sanitize-error.js";
4
+ const PERSISTENCE_REST = "https://rest.cosmos.directory/persistence";
5
+ // rpc.cosmos.directory returns 401 on WebSocket connections needed by SigningStargateClient
6
+ const PERSISTENCE_RPC = "https://persistence-rpc.polkachu.com";
7
+ const TIMEOUT_MS = 30_000;
8
+ async function fetchJson(url, init) {
9
+ const controller = new AbortController();
10
+ const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
11
+ try {
12
+ const res = await fetch(url, { ...init, signal: controller.signal });
13
+ if (!res.ok) {
14
+ const text = await res.text().catch(() => "");
15
+ throw new Error(`HTTP ${res.status}: ${text.slice(0, 200)}`);
16
+ }
17
+ return res.json();
18
+ }
19
+ finally {
20
+ clearTimeout(timer);
21
+ }
22
+ }
23
+ /**
24
+ * Fetch multiplier from the rewards API (canonical source of tier data).
25
+ * Falls back to hardcoded tiers if the API is unavailable.
26
+ */
27
+ async function getMultiplierFromApi(persistenceAddress) {
28
+ try {
29
+ const today = new Date().toISOString().slice(0, 10);
30
+ const tierData = await fetchJson(`https://rewards.interop.persistence.one/tiers/${persistenceAddress}?blockDate=${today}`);
31
+ const result = {
32
+ multiplier: tierData.multiplier ? `${tierData.multiplier}x` : "1x",
33
+ tier: tierData.tier || "Explorer",
34
+ };
35
+ if (tierData.nextMultiplierMilestone) {
36
+ result.nextMultiplier = `${tierData.nextMultiplierMilestone.multiplier}x`;
37
+ result.stakeNeeded = tierData.nextMultiplierMilestone.stake;
38
+ }
39
+ return result;
40
+ }
41
+ catch {
42
+ return { multiplier: "unknown", tier: "unknown" };
43
+ }
44
+ }
45
+ /**
46
+ * Get the best validator to delegate to (highest APR, active, not jailed)
47
+ */
48
+ async function getBestValidator() {
49
+ try {
50
+ // Get all validators
51
+ const validatorsData = await fetchJson(`${PERSISTENCE_REST}/cosmos/staking/v1beta1/validators?status=BOND_STATUS_BONDED&pagination.limit=200`);
52
+ const validators = validatorsData.validators || [];
53
+ if (validators.length === 0) {
54
+ throw new Error("No active validators found");
55
+ }
56
+ // For now, pick a well-known validator. In the future, we could fetch APR data
57
+ const persistenceValidators = validators.filter((v) => !v.jailed &&
58
+ v.status === "BOND_STATUS_BONDED" &&
59
+ v.description?.moniker);
60
+ if (persistenceValidators.length === 0) {
61
+ throw new Error("No unjailed bonded validators found");
62
+ }
63
+ // Sort by voting power (descending) and pick a top one
64
+ persistenceValidators.sort((a, b) => parseInt(b.tokens || "0") - parseInt(a.tokens || "0"));
65
+ const chosen = persistenceValidators[0];
66
+ return {
67
+ address: chosen.operator_address,
68
+ moniker: chosen.description?.moniker || "Unknown Validator",
69
+ apr: undefined // Could be fetched from external APIs in the future
70
+ };
71
+ }
72
+ catch (err) {
73
+ // Fallback to a known validator if API fails
74
+ return {
75
+ address: "persistencevaloper1aw32k4t8qn3wva5g7vqqajr4qj7zdt2a0dmqn5", // Example validator
76
+ moniker: "Persistence Foundation",
77
+ };
78
+ }
79
+ }
80
+ /**
81
+ * Get the account sequence number for transaction signing
82
+ */
83
+ async function getAccountInfo(address) {
84
+ const data = await fetchJson(`${PERSISTENCE_REST}/cosmos/auth/v1beta1/accounts/${address}`);
85
+ const account = data.account;
86
+ return {
87
+ accountNumber: account.account_number || "0",
88
+ sequence: account.sequence || "0",
89
+ };
90
+ }
91
+ /**
92
+ * Broadcast a signed transaction to the Persistence network
93
+ */
94
+ async function broadcastTransaction(txBytes) {
95
+ const body = {
96
+ tx_bytes: txBytes,
97
+ mode: "BROADCAST_MODE_SYNC", // Use sync mode for immediate response
98
+ };
99
+ const response = await fetchJson(`${PERSISTENCE_REST}/cosmos/tx/v1beta1/txs`, {
100
+ method: "POST",
101
+ headers: { "Content-Type": "application/json" },
102
+ body: JSON.stringify(body),
103
+ });
104
+ return {
105
+ txHash: response.tx_response?.txhash || "unknown",
106
+ code: response.tx_response?.code || 0,
107
+ rawLog: response.tx_response?.raw_log,
108
+ };
109
+ }
110
+ export function registerXprtStakingTools(server) {
111
+ // ─── xprt_stake ──────────────────────────────────────────────────────────
112
+ server.tool("xprt_stake", "Delegate liquid XPRT to a validator to earn staking rewards and increase farming multiplier. " +
113
+ "Multiplier tiers: 1x (default), 2x (≥10,000 staked), 5x (≥1,000,000 staked). " +
114
+ "Staked tokens continue earning farming rewards with the multiplier applied.", {
115
+ amount: z.string().describe("Amount of XPRT to stake, or 'max' for all available liquid XPRT"),
116
+ validatorAddress: z.string().optional().describe("Validator address (persistencevaloper1...). If omitted, auto-selects a high-performing active validator"),
117
+ dryRun: z.boolean().default(true).describe("Preview the operation without executing (default: true)"),
118
+ }, async (params) => {
119
+ const mnemonic = getKey("mnemonic");
120
+ if (!mnemonic) {
121
+ return {
122
+ content: [{ type: "text", text: "No mnemonic found. Run wallet_setup or add MNEMONIC to .env file." }],
123
+ isError: true,
124
+ };
125
+ }
126
+ try {
127
+ const { Secp256k1HdWallet } = await import("@cosmjs/amino");
128
+ const { SigningStargateClient } = await import("@cosmjs/stargate");
129
+ const { MsgDelegate } = await import("cosmjs-types/cosmos/staking/v1beta1/tx");
130
+ const wallet = await Secp256k1HdWallet.fromMnemonic(mnemonic, { prefix: "persistence" });
131
+ const [account] = await wallet.getAccounts();
132
+ const delegatorAddress = account.address;
133
+ // Get current balance
134
+ const balanceData = await fetchJson(`${PERSISTENCE_REST}/cosmos/bank/v1beta1/balances/${delegatorAddress}`);
135
+ const xprtBalance = balanceData.balances?.find((b) => b.denom === "uxprt");
136
+ const liquidAmount = xprtBalance ? parseInt(xprtBalance.amount) : 0;
137
+ if (liquidAmount === 0) {
138
+ return {
139
+ content: [{ type: "text", text: "No liquid XPRT available to stake. Check your wallet balance." }],
140
+ isError: true,
141
+ };
142
+ }
143
+ // Parse amount
144
+ let amountToStake;
145
+ if (params.amount.toLowerCase() === "max") {
146
+ amountToStake = liquidAmount;
147
+ }
148
+ else {
149
+ const parsed = parseFloat(params.amount);
150
+ if (isNaN(parsed) || parsed <= 0) {
151
+ return {
152
+ content: [{ type: "text", text: "Invalid amount. Use a positive number or 'max'." }],
153
+ isError: true,
154
+ };
155
+ }
156
+ amountToStake = Math.floor(parsed * 1e6); // Convert to uxprt
157
+ }
158
+ if (amountToStake > liquidAmount) {
159
+ return {
160
+ content: [{ type: "text", text: `Insufficient balance. Available: ${(liquidAmount / 1e6).toFixed(6)} XPRT, Requested: ${(amountToStake / 1e6).toFixed(6)} XPRT` }],
161
+ isError: true,
162
+ };
163
+ }
164
+ // Get validator info
165
+ let validatorAddress = params.validatorAddress;
166
+ let validatorMoniker = "Unknown";
167
+ if (!validatorAddress) {
168
+ const bestValidator = await getBestValidator();
169
+ validatorAddress = bestValidator.address;
170
+ validatorMoniker = bestValidator.moniker;
171
+ }
172
+ else {
173
+ // Validate and get validator info
174
+ try {
175
+ const valData = await fetchJson(`${PERSISTENCE_REST}/cosmos/staking/v1beta1/validators/${validatorAddress}`);
176
+ validatorMoniker = valData.validator?.description?.moniker || "Unknown";
177
+ if (valData.validator?.jailed) {
178
+ return {
179
+ content: [{ type: "text", text: `Validator ${validatorAddress} is jailed and cannot receive delegations.` }],
180
+ isError: true,
181
+ };
182
+ }
183
+ }
184
+ catch {
185
+ return {
186
+ content: [{ type: "text", text: `Validator ${validatorAddress} not found or inactive.` }],
187
+ isError: true,
188
+ };
189
+ }
190
+ }
191
+ // Get current staking info for multiplier calculation
192
+ let currentStaked = 0;
193
+ try {
194
+ const delegationsData = await fetchJson(`${PERSISTENCE_REST}/cosmos/staking/v1beta1/delegations/${delegatorAddress}`);
195
+ if (delegationsData.delegation_responses) {
196
+ for (const del of delegationsData.delegation_responses) {
197
+ currentStaked += parseInt(del.balance?.amount || "0");
198
+ }
199
+ }
200
+ }
201
+ catch {
202
+ // Continue with 0 if delegations fetch fails
203
+ }
204
+ const newTotalStaked = (currentStaked + amountToStake) / 1e6;
205
+ // Get multiplier from rewards API (canonical source)
206
+ const tierInfo = await getMultiplierFromApi(delegatorAddress);
207
+ const currentMultiplier = tierInfo.multiplier;
208
+ // Estimate new multiplier (will be recalculated by API after staking)
209
+ const newMultiplier = tierInfo.nextMultiplier && newTotalStaked >= (tierInfo.stakeNeeded || Infinity)
210
+ ? tierInfo.nextMultiplier
211
+ : currentMultiplier;
212
+ if (params.dryRun) {
213
+ return {
214
+ content: [{
215
+ type: "text",
216
+ text: JSON.stringify({
217
+ dryRun: true,
218
+ operation: "delegate",
219
+ amountToStake: `${(amountToStake / 1e6).toFixed(6)} XPRT`,
220
+ validator: {
221
+ address: validatorAddress,
222
+ moniker: validatorMoniker,
223
+ },
224
+ currentStaking: {
225
+ amount: `${(currentStaked / 1e6).toFixed(2)} XPRT`,
226
+ multiplier: currentMultiplier,
227
+ },
228
+ afterStaking: {
229
+ totalStaked: `${newTotalStaked.toFixed(2)} XPRT`,
230
+ newMultiplier: newMultiplier,
231
+ multiplierChange: currentMultiplier !== newMultiplier ? `${currentMultiplier} → ${newMultiplier}` : "no change",
232
+ },
233
+ estimatedApr: "~15-20% (variable)",
234
+ unbondingPeriod: "21 days",
235
+ note: "Set dryRun=false to execute this delegation.",
236
+ }, null, 2),
237
+ }],
238
+ };
239
+ }
240
+ // Execute the staking transaction
241
+ const client = await SigningStargateClient.connectWithSigner(PERSISTENCE_RPC, wallet);
242
+ const msg = {
243
+ typeUrl: "/cosmos.staking.v1beta1.MsgDelegate",
244
+ value: MsgDelegate.fromPartial({
245
+ delegatorAddress: delegatorAddress,
246
+ validatorAddress: validatorAddress,
247
+ amount: {
248
+ denom: "uxprt",
249
+ amount: amountToStake.toString(),
250
+ },
251
+ }),
252
+ };
253
+ const fee = {
254
+ amount: [{ denom: "uxprt", amount: "5000" }], // 0.005 XPRT fee
255
+ gas: "300000",
256
+ };
257
+ const result = await client.signAndBroadcast(delegatorAddress, [msg], fee, "Delegate XPRT via BridgeKitty");
258
+ if (result.code !== 0) {
259
+ throw new Error(`Transaction failed with code ${result.code}: ${result.rawLog}`);
260
+ }
261
+ return {
262
+ content: [{
263
+ type: "text",
264
+ text: JSON.stringify({
265
+ status: "success",
266
+ operation: "delegate",
267
+ txHash: result.transactionHash,
268
+ amountStaked: `${(amountToStake / 1e6).toFixed(6)} XPRT`,
269
+ validator: {
270
+ address: validatorAddress,
271
+ moniker: validatorMoniker,
272
+ },
273
+ newTotalStaked: `${newTotalStaked.toFixed(2)} XPRT`,
274
+ newMultiplier: newMultiplier,
275
+ multiplierChange: currentMultiplier !== newMultiplier ? `${currentMultiplier} → ${newMultiplier}` : "no change",
276
+ unbondingPeriod: "21 days",
277
+ blockExplorer: `https://mintscan.io/persistence/tx/${result.transactionHash}`,
278
+ }, null, 2),
279
+ }],
280
+ };
281
+ }
282
+ catch (err) {
283
+ return {
284
+ content: [{ type: "text", text: `Staking failed: ${sanitizeError(err)}` }],
285
+ isError: true,
286
+ };
287
+ }
288
+ });
289
+ // ─── xprt_unstake ────────────────────────────────────────────────────────
290
+ server.tool("xprt_unstake", "Initiate unbonding (unstaking) of staked XPRT. Tokens will be unavailable for 21 days during unbonding. " +
291
+ "Unbonding tokens do not earn staking rewards and do not count toward farming multiplier.", {
292
+ amount: z.string().describe("Amount of XPRT to unstake"),
293
+ validatorAddress: z.string().describe("Validator address to undelegate from (persistencevaloper1...)"),
294
+ dryRun: z.boolean().default(true).describe("Preview the operation without executing (default: true)"),
295
+ }, async (params) => {
296
+ const mnemonic = getKey("mnemonic");
297
+ if (!mnemonic) {
298
+ return {
299
+ content: [{ type: "text", text: "No mnemonic found. Run wallet_setup or add MNEMONIC to .env file." }],
300
+ isError: true,
301
+ };
302
+ }
303
+ try {
304
+ const { Secp256k1HdWallet } = await import("@cosmjs/amino");
305
+ const { SigningStargateClient } = await import("@cosmjs/stargate");
306
+ const { MsgUndelegate } = await import("cosmjs-types/cosmos/staking/v1beta1/tx");
307
+ const wallet = await Secp256k1HdWallet.fromMnemonic(mnemonic, { prefix: "persistence" });
308
+ const [account] = await wallet.getAccounts();
309
+ const delegatorAddress = account.address;
310
+ // Parse amount
311
+ const parsed = parseFloat(params.amount);
312
+ if (isNaN(parsed) || parsed <= 0) {
313
+ return {
314
+ content: [{ type: "text", text: "Invalid amount. Use a positive number." }],
315
+ isError: true,
316
+ };
317
+ }
318
+ const amountToUnstake = Math.floor(parsed * 1e6); // Convert to uxprt
319
+ // Check current delegation to this validator
320
+ let delegatedAmount = 0;
321
+ try {
322
+ const delData = await fetchJson(`${PERSISTENCE_REST}/cosmos/staking/v1beta1/delegations/${delegatorAddress}/${params.validatorAddress}`);
323
+ delegatedAmount = parseInt(delData.delegation_response?.balance?.amount || "0");
324
+ }
325
+ catch {
326
+ return {
327
+ content: [{ type: "text", text: `No delegation found to validator ${params.validatorAddress}` }],
328
+ isError: true,
329
+ };
330
+ }
331
+ if (amountToUnstake > delegatedAmount) {
332
+ return {
333
+ content: [{ type: "text", text: `Insufficient delegation. Available: ${(delegatedAmount / 1e6).toFixed(6)} XPRT, Requested: ${(amountToUnstake / 1e6).toFixed(6)} XPRT` }],
334
+ isError: true,
335
+ };
336
+ }
337
+ // Get validator info
338
+ let validatorMoniker = "Unknown";
339
+ try {
340
+ const valData = await fetchJson(`${PERSISTENCE_REST}/cosmos/staking/v1beta1/validators/${params.validatorAddress}`);
341
+ validatorMoniker = valData.validator?.description?.moniker || "Unknown";
342
+ }
343
+ catch {
344
+ // Continue with unknown moniker
345
+ }
346
+ // Calculate new multiplier after unstaking
347
+ let totalStaked = 0;
348
+ try {
349
+ const delegationsData = await fetchJson(`${PERSISTENCE_REST}/cosmos/staking/v1beta1/delegations/${delegatorAddress}`);
350
+ if (delegationsData.delegation_responses) {
351
+ for (const del of delegationsData.delegation_responses) {
352
+ totalStaked += parseInt(del.balance?.amount || "0");
353
+ }
354
+ }
355
+ }
356
+ catch {
357
+ // Continue with current delegation only
358
+ totalStaked = delegatedAmount;
359
+ }
360
+ // Get multiplier from rewards API (canonical source)
361
+ const unstakeTierInfo = await getMultiplierFromApi(delegatorAddress);
362
+ const currentMultiplier = unstakeTierInfo.multiplier;
363
+ const newTotalStaked = (totalStaked - amountToUnstake) / 1e6;
364
+ // After unstaking, multiplier will likely decrease — estimate conservatively
365
+ const newMultiplier = "recalculated after unstaking";
366
+ const completionDate = new Date();
367
+ completionDate.setDate(completionDate.getDate() + 21);
368
+ if (params.dryRun) {
369
+ return {
370
+ content: [{
371
+ type: "text",
372
+ text: JSON.stringify({
373
+ dryRun: true,
374
+ operation: "undelegate",
375
+ amountToUnstake: `${(amountToUnstake / 1e6).toFixed(6)} XPRT`,
376
+ validator: {
377
+ address: params.validatorAddress,
378
+ moniker: validatorMoniker,
379
+ },
380
+ unbondingPeriod: "21 days",
381
+ completionDate: completionDate.toISOString().split('T')[0],
382
+ currentStaking: {
383
+ total: `${(totalStaked / 1e6).toFixed(2)} XPRT`,
384
+ multiplier: currentMultiplier,
385
+ },
386
+ afterUnstaking: {
387
+ totalStaked: `${newTotalStaked.toFixed(2)} XPRT`,
388
+ newMultiplier: newMultiplier,
389
+ multiplierChange: currentMultiplier !== newMultiplier ? `${currentMultiplier} → ${newMultiplier}` : "no change",
390
+ },
391
+ warning: "Unbonding tokens do not earn rewards and do not count toward farming multiplier during the 21-day period.",
392
+ note: "Set dryRun=false to execute this undelegation.",
393
+ }, null, 2),
394
+ }],
395
+ };
396
+ }
397
+ // Execute the unstaking transaction
398
+ const client = await SigningStargateClient.connectWithSigner(PERSISTENCE_RPC, wallet);
399
+ const msg = {
400
+ typeUrl: "/cosmos.staking.v1beta1.MsgUndelegate",
401
+ value: MsgUndelegate.fromPartial({
402
+ delegatorAddress: delegatorAddress,
403
+ validatorAddress: params.validatorAddress,
404
+ amount: {
405
+ denom: "uxprt",
406
+ amount: amountToUnstake.toString(),
407
+ },
408
+ }),
409
+ };
410
+ const fee = {
411
+ amount: [{ denom: "uxprt", amount: "5000" }], // 0.005 XPRT fee
412
+ gas: "300000",
413
+ };
414
+ const result = await client.signAndBroadcast(delegatorAddress, [msg], fee, "Undelegate XPRT via BridgeKitty");
415
+ if (result.code !== 0) {
416
+ throw new Error(`Transaction failed with code ${result.code}: ${result.rawLog}`);
417
+ }
418
+ return {
419
+ content: [{
420
+ type: "text",
421
+ text: JSON.stringify({
422
+ status: "success",
423
+ operation: "undelegate",
424
+ txHash: result.transactionHash,
425
+ amountUnstaked: `${(amountToUnstake / 1e6).toFixed(6)} XPRT`,
426
+ validator: {
427
+ address: params.validatorAddress,
428
+ moniker: validatorMoniker,
429
+ },
430
+ unbondingPeriod: "21 days",
431
+ completionDate: completionDate.toISOString().split('T')[0],
432
+ newTotalStaked: `${newTotalStaked.toFixed(2)} XPRT`,
433
+ newMultiplier: newMultiplier,
434
+ multiplierChange: currentMultiplier !== newMultiplier ? `${currentMultiplier} → ${newMultiplier}` : "no change",
435
+ blockExplorer: `https://mintscan.io/persistence/tx/${result.transactionHash}`,
436
+ }, null, 2),
437
+ }],
438
+ };
439
+ }
440
+ catch (err) {
441
+ return {
442
+ content: [{ type: "text", text: `Unstaking failed: ${sanitizeError(err)}` }],
443
+ isError: true,
444
+ };
445
+ }
446
+ });
447
+ // ─── xprt_claim_rewards ─────────────────────────────────────────────────
448
+ server.tool("xprt_claim_rewards", "Claim all pending staking rewards from delegated XPRT. Rewards are liquid XPRT that can be spent, bridged, or re-staked.", {
449
+ dryRun: z.boolean().default(true).describe("Preview the operation without executing (default: true)"),
450
+ }, async (params) => {
451
+ const mnemonic = getKey("mnemonic");
452
+ if (!mnemonic) {
453
+ return {
454
+ content: [{ type: "text", text: "No mnemonic found. Run wallet_setup or add MNEMONIC to .env file." }],
455
+ isError: true,
456
+ };
457
+ }
458
+ try {
459
+ const { Secp256k1HdWallet } = await import("@cosmjs/amino");
460
+ const { SigningStargateClient } = await import("@cosmjs/stargate");
461
+ const { MsgWithdrawDelegatorReward } = await import("cosmjs-types/cosmos/distribution/v1beta1/tx");
462
+ const wallet = await Secp256k1HdWallet.fromMnemonic(mnemonic, { prefix: "persistence" });
463
+ const [account] = await wallet.getAccounts();
464
+ const delegatorAddress = account.address;
465
+ // Get pending rewards
466
+ const rewardsData = await fetchJson(`${PERSISTENCE_REST}/cosmos/distribution/v1beta1/delegators/${delegatorAddress}/rewards`);
467
+ const rewards = rewardsData.rewards || [];
468
+ if (rewards.length === 0) {
469
+ return {
470
+ content: [{ type: "text", text: "No pending staking rewards to claim." }],
471
+ isError: true,
472
+ };
473
+ }
474
+ let totalRewards = 0;
475
+ const validators = [];
476
+ for (const reward of rewards) {
477
+ const validatorAddress = reward.validator_address;
478
+ let validatorMoniker = "Unknown";
479
+ try {
480
+ const valData = await fetchJson(`${PERSISTENCE_REST}/cosmos/staking/v1beta1/validators/${validatorAddress}`);
481
+ validatorMoniker = valData.validator?.description?.moniker || "Unknown";
482
+ }
483
+ catch {
484
+ // Continue with unknown moniker
485
+ }
486
+ let validatorReward = 0;
487
+ for (const coin of reward.reward || []) {
488
+ if (coin.denom === "uxprt") {
489
+ validatorReward += parseFloat(coin.amount || "0");
490
+ }
491
+ }
492
+ if (validatorReward > 0) {
493
+ totalRewards += validatorReward;
494
+ validators.push({
495
+ address: validatorAddress,
496
+ moniker: validatorMoniker,
497
+ rewards: (validatorReward / 1e6).toFixed(6),
498
+ });
499
+ }
500
+ }
501
+ if (totalRewards < 1) { // Less than 1 uxprt (0.000001 XPRT)
502
+ return {
503
+ content: [{ type: "text", text: "Pending rewards are too small to claim (less than 0.000001 XPRT)." }],
504
+ isError: true,
505
+ };
506
+ }
507
+ if (params.dryRun) {
508
+ return {
509
+ content: [{
510
+ type: "text",
511
+ text: JSON.stringify({
512
+ dryRun: true,
513
+ operation: "withdraw_rewards",
514
+ totalRewards: `${(totalRewards / 1e6).toFixed(6)} XPRT`,
515
+ validatorCount: validators.length,
516
+ validators: validators,
517
+ estimatedFee: "~0.005 XPRT",
518
+ netRewards: `${((totalRewards - 5000) / 1e6).toFixed(6)} XPRT`,
519
+ note: "Set dryRun=false to claim these rewards.",
520
+ }, null, 2),
521
+ }],
522
+ };
523
+ }
524
+ // Execute reward claim
525
+ const client = await SigningStargateClient.connectWithSigner(PERSISTENCE_RPC, wallet);
526
+ const msgs = validators.map(v => ({
527
+ typeUrl: "/cosmos.distribution.v1beta1.MsgWithdrawDelegatorReward",
528
+ value: MsgWithdrawDelegatorReward.fromPartial({
529
+ delegatorAddress: delegatorAddress,
530
+ validatorAddress: v.address,
531
+ }),
532
+ }));
533
+ const fee = {
534
+ amount: [{ denom: "uxprt", amount: "5000" }], // 0.005 XPRT fee
535
+ gas: "300000", // Higher gas for multiple messages
536
+ };
537
+ const result = await client.signAndBroadcast(delegatorAddress, msgs, fee, "Claim staking rewards via BridgeKitty");
538
+ if (result.code !== 0) {
539
+ throw new Error(`Transaction failed with code ${result.code}: ${result.rawLog}`);
540
+ }
541
+ return {
542
+ content: [{
543
+ type: "text",
544
+ text: JSON.stringify({
545
+ status: "success",
546
+ operation: "withdraw_rewards",
547
+ txHash: result.transactionHash,
548
+ rewardsClaimed: `${(totalRewards / 1e6).toFixed(6)} XPRT`,
549
+ validatorCount: validators.length,
550
+ validators: validators,
551
+ fee: "0.005 XPRT",
552
+ netRewards: `${((totalRewards - 5000) / 1e6).toFixed(6)} XPRT`,
553
+ blockExplorer: `https://mintscan.io/persistence/tx/${result.transactionHash}`,
554
+ }, null, 2),
555
+ }],
556
+ };
557
+ }
558
+ catch (err) {
559
+ return {
560
+ content: [{ type: "text", text: `Claiming rewards failed: ${sanitizeError(err)}` }],
561
+ isError: true,
562
+ };
563
+ }
564
+ });
565
+ }
@@ -0,0 +1,22 @@
1
+ export interface ChainEntry {
2
+ id: number;
3
+ name: string;
4
+ key: string;
5
+ }
6
+ export declare const PERSISTENCE_CHAIN_ID = 9999001;
7
+ export declare const COSMOSHUB_CHAIN_ID = 9999002;
8
+ export declare const SOLANA_CHAIN_ID = 7565164;
9
+ export declare function getBackendChainId(backendName: string, chainId: number): number;
10
+ export declare const COSMOS_CHAIN_IDS: Record<string, string>;
11
+ export declare const SYNTHETIC_TO_COSMOS: Record<number, string>;
12
+ export declare function resolveChainId(input: string): number | null;
13
+ /** Check if a chain key or ID refers to Solana */
14
+ export declare function isSolanaChain(chainKeyOrId: string | number): boolean;
15
+ /** Check if a chain key or ID refers to a Cosmos chain */
16
+ export declare function isCosmosChain(chainKeyOrId: string | number): boolean;
17
+ /** Get the Cosmos chain ID string for Squid Router from a synthetic numeric ID */
18
+ export declare function getCosmosChainIdFromSynthetic(syntheticId: number): string | null;
19
+ /** Get the Cosmos chain ID string from a chain key */
20
+ export declare function getCosmosChainId(key: string): string | null;
21
+ export declare function getChainName(id: number): string;
22
+ export declare function getAllChains(): ChainEntry[];