@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.
- package/LICENSE +21 -0
- package/README.md +232 -0
- package/dist/backends/across.d.ts +10 -0
- package/dist/backends/across.js +285 -0
- package/dist/backends/debridge.d.ts +11 -0
- package/dist/backends/debridge.js +380 -0
- package/dist/backends/lifi.d.ts +19 -0
- package/dist/backends/lifi.js +295 -0
- package/dist/backends/persistence.d.ts +86 -0
- package/dist/backends/persistence.js +642 -0
- package/dist/backends/relay.d.ts +11 -0
- package/dist/backends/relay.js +292 -0
- package/dist/backends/squid.d.ts +31 -0
- package/dist/backends/squid.js +476 -0
- package/dist/backends/types.d.ts +125 -0
- package/dist/backends/types.js +11 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +154 -0
- package/dist/routing/engine.d.ts +49 -0
- package/dist/routing/engine.js +336 -0
- package/dist/tools/check-status.d.ts +3 -0
- package/dist/tools/check-status.js +93 -0
- package/dist/tools/execute-bridge.d.ts +3 -0
- package/dist/tools/execute-bridge.js +428 -0
- package/dist/tools/get-chains.d.ts +3 -0
- package/dist/tools/get-chains.js +162 -0
- package/dist/tools/get-quote.d.ts +3 -0
- package/dist/tools/get-quote.js +534 -0
- package/dist/tools/get-tokens.d.ts +3 -0
- package/dist/tools/get-tokens.js +128 -0
- package/dist/tools/help.d.ts +2 -0
- package/dist/tools/help.js +204 -0
- package/dist/tools/multi-quote.d.ts +3 -0
- package/dist/tools/multi-quote.js +310 -0
- package/dist/tools/onboard.d.ts +3 -0
- package/dist/tools/onboard.js +218 -0
- package/dist/tools/wallet.d.ts +14 -0
- package/dist/tools/wallet.js +744 -0
- package/dist/tools/xprt-farm.d.ts +3 -0
- package/dist/tools/xprt-farm.js +1308 -0
- package/dist/tools/xprt-rewards.d.ts +2 -0
- package/dist/tools/xprt-rewards.js +177 -0
- package/dist/tools/xprt-staking.d.ts +2 -0
- package/dist/tools/xprt-staking.js +565 -0
- package/dist/utils/chains.d.ts +22 -0
- package/dist/utils/chains.js +154 -0
- package/dist/utils/circuit-breaker.d.ts +64 -0
- package/dist/utils/circuit-breaker.js +160 -0
- package/dist/utils/evm.d.ts +18 -0
- package/dist/utils/evm.js +46 -0
- package/dist/utils/fill-detector.d.ts +70 -0
- package/dist/utils/fill-detector.js +298 -0
- package/dist/utils/gas-estimator.d.ts +67 -0
- package/dist/utils/gas-estimator.js +340 -0
- package/dist/utils/sanitize-error.d.ts +23 -0
- package/dist/utils/sanitize-error.js +101 -0
- package/dist/utils/token-registry.d.ts +70 -0
- package/dist/utils/token-registry.js +669 -0
- package/dist/utils/tokens.d.ts +17 -0
- package/dist/utils/tokens.js +37 -0
- package/dist/utils/tx-simulator.d.ts +27 -0
- package/dist/utils/tx-simulator.js +105 -0
- 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[];
|