@aibtc/mcp-server 1.33.3 → 1.34.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/dist/config/contracts.d.ts +35 -32
- package/dist/config/contracts.d.ts.map +1 -1
- package/dist/config/contracts.js +58 -102
- package/dist/config/contracts.js.map +1 -1
- package/dist/config/pillar.d.ts.map +1 -1
- package/dist/config/pillar.js +3 -1
- package/dist/config/pillar.js.map +1 -1
- package/dist/index.js +32 -20
- package/dist/index.js.map +1 -1
- package/dist/services/defi.service.d.ts +73 -40
- package/dist/services/defi.service.d.ts.map +1 -1
- package/dist/services/defi.service.js +237 -263
- package/dist/services/defi.service.js.map +1 -1
- package/dist/tools/defi.tools.d.ts.map +1 -1
- package/dist/tools/defi.tools.js +40 -80
- package/dist/tools/defi.tools.js.map +1 -1
- package/dist/tools/index.d.ts +1 -8
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +36 -63
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/pillar-direct.tools.d.ts.map +1 -1
- package/dist/tools/pillar-direct.tools.js +100 -113
- package/dist/tools/pillar-direct.tools.js.map +1 -1
- package/dist/tools/styx.tools.d.ts +20 -0
- package/dist/tools/styx.tools.d.ts.map +1 -0
- package/dist/tools/styx.tools.js +401 -0
- package/dist/tools/styx.tools.js.map +1 -0
- package/dist/tools/yield-hunter.tools.d.ts.map +1 -1
- package/dist/tools/yield-hunter.tools.js +15 -18
- package/dist/tools/yield-hunter.tools.js.map +1 -1
- package/dist/utils/validation.d.ts +1 -1
- package/dist/yield-hunter/index.js +2 -2
- package/dist/yield-hunter/index.js.map +1 -1
- package/package.json +2 -1
- package/skill/SKILL.md +1 -1
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { uintCV, contractPrincipalCV, cvToJSON, hexToCV, PostConditionMode, Pc, principalCV, broadcastTransaction, makeContractCall,
|
|
1
|
+
import { uintCV, contractPrincipalCV, cvToJSON, hexToCV, PostConditionMode, Pc, principalCV, broadcastTransaction, makeContractCall, noneCV, someCV, } from "@stacks/transactions";
|
|
2
2
|
import { STACKS_MAINNET } from "@stacks/network";
|
|
3
3
|
import { AlexSDK, Currency } from "alex-sdk";
|
|
4
4
|
import { getHiroApi } from "./hiro-api.js";
|
|
5
|
-
import { getAlexContracts, getZestContracts, parseContractId, ZEST_ASSETS,
|
|
5
|
+
import { getAlexContracts, getZestContracts, parseContractId, ZEST_ASSETS, ZEST_V2_MARKET, ZEST_V2_MARKET_VAULT, } from "../config/index.js";
|
|
6
6
|
import { callContract } from "../transactions/builder.js";
|
|
7
7
|
// ============================================================================
|
|
8
8
|
// ALEX DEX Service (using alex-sdk)
|
|
@@ -212,13 +212,12 @@ export class AlexDexService {
|
|
|
212
212
|
}
|
|
213
213
|
}
|
|
214
214
|
// ============================================================================
|
|
215
|
-
// Zest Protocol Service
|
|
215
|
+
// Zest Protocol v2 Service
|
|
216
216
|
// ============================================================================
|
|
217
217
|
export class ZestProtocolService {
|
|
218
218
|
network;
|
|
219
219
|
hiro;
|
|
220
220
|
contracts;
|
|
221
|
-
assetsListCache = null;
|
|
222
221
|
constructor(network) {
|
|
223
222
|
this.network = network;
|
|
224
223
|
this.hiro = getHiroApi(network);
|
|
@@ -244,79 +243,7 @@ export class ZestProtocolService {
|
|
|
244
243
|
throw new Error(`Unknown Zest asset: ${assetOrSymbol}. Use zest_list_assets to see available assets.`);
|
|
245
244
|
}
|
|
246
245
|
/**
|
|
247
|
-
*
|
|
248
|
-
* This is a list of tuples containing (asset, lp-token, oracle) for all supported assets
|
|
249
|
-
* Result is cached since ZEST_ASSETS_LIST is static
|
|
250
|
-
*/
|
|
251
|
-
buildAssetsListCV() {
|
|
252
|
-
if (this.assetsListCache) {
|
|
253
|
-
return this.assetsListCache;
|
|
254
|
-
}
|
|
255
|
-
this.assetsListCache = listCV(ZEST_ASSETS_LIST.map((asset) => {
|
|
256
|
-
const [assetAddr, assetName] = parseContractIdTuple(asset.token);
|
|
257
|
-
const [lpAddr, lpName] = parseContractIdTuple(asset.lpToken);
|
|
258
|
-
const [oracleAddr, oracleName] = parseContractIdTuple(asset.oracle);
|
|
259
|
-
return tupleCV({
|
|
260
|
-
asset: contractPrincipalCV(assetAddr, assetName),
|
|
261
|
-
"lp-token": contractPrincipalCV(lpAddr, lpName),
|
|
262
|
-
oracle: contractPrincipalCV(oracleAddr, oracleName),
|
|
263
|
-
});
|
|
264
|
-
}));
|
|
265
|
-
return this.assetsListCache;
|
|
266
|
-
}
|
|
267
|
-
/**
|
|
268
|
-
* Pyth price feed IDs used by Zest's oracle contracts.
|
|
269
|
-
* BTC and STX feeds cover all current Zest assets.
|
|
270
|
-
*/
|
|
271
|
-
// BTC/USD and STX/USD are sufficient for all current Zest assets.
|
|
272
|
-
// Stablecoin assets (aeUSDC, sUSDT, USDA, USDh) use on-chain fixed-price oracles
|
|
273
|
-
// rather than Pyth feeds, so no additional feed IDs are needed here.
|
|
274
|
-
static PYTH_FEED_IDS = [
|
|
275
|
-
"0xe62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43", // BTC/USD
|
|
276
|
-
"0xec7a775f46379b5e943c3526b1c8d54cd49749176b0b98e02dde68d1bd335c17", // STX/USD
|
|
277
|
-
];
|
|
278
|
-
priceFeedCache = null;
|
|
279
|
-
static PRICE_FEED_TTL_MS = 30_000; // 30s cache — oracle rejects >360s
|
|
280
|
-
/**
|
|
281
|
-
* Fetch fresh Pyth price update VAA from Hermes API.
|
|
282
|
-
* Caches for 30s to avoid redundant requests when multiple ops run in sequence.
|
|
283
|
-
* Returns someCV(bufferCV(...)) for the price-feed-bytes parameter,
|
|
284
|
-
* or noneCV() if the fetch fails (graceful degradation).
|
|
285
|
-
*/
|
|
286
|
-
async fetchPriceFeedBytes() {
|
|
287
|
-
if (this.priceFeedCache && Date.now() - this.priceFeedCache.timestamp < ZestProtocolService.PRICE_FEED_TTL_MS) {
|
|
288
|
-
return this.priceFeedCache.value;
|
|
289
|
-
}
|
|
290
|
-
try {
|
|
291
|
-
const ids = ZestProtocolService.PYTH_FEED_IDS
|
|
292
|
-
.map((id) => `ids[]=${id}`)
|
|
293
|
-
.join("&");
|
|
294
|
-
const controller = new AbortController();
|
|
295
|
-
const timeout = setTimeout(() => controller.abort(), 5_000);
|
|
296
|
-
const res = await fetch(`https://hermes.pyth.network/v2/updates/price/latest?${ids}&encoding=hex`, { signal: controller.signal });
|
|
297
|
-
clearTimeout(timeout);
|
|
298
|
-
if (!res.ok) {
|
|
299
|
-
console.warn(`Pyth Hermes returned HTTP ${res.status}, falling back to noneCV()`);
|
|
300
|
-
return noneCV();
|
|
301
|
-
}
|
|
302
|
-
const data = await res.json();
|
|
303
|
-
const hex = data?.binary?.data?.[0];
|
|
304
|
-
if (!hex) {
|
|
305
|
-
console.warn("Pyth Hermes response missing binary data, falling back to noneCV()");
|
|
306
|
-
return noneCV();
|
|
307
|
-
}
|
|
308
|
-
const value = someCV(bufferCV(Buffer.from(hex, "hex")));
|
|
309
|
-
this.priceFeedCache = { value, timestamp: Date.now() };
|
|
310
|
-
return value;
|
|
311
|
-
}
|
|
312
|
-
catch (err) {
|
|
313
|
-
console.warn("Failed to fetch Pyth price feed, falling back to noneCV():", err);
|
|
314
|
-
return noneCV();
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
/**
|
|
318
|
-
* Get all supported assets from Zest Protocol
|
|
319
|
-
* Returns the hardcoded asset list with full metadata
|
|
246
|
+
* Get all supported assets from Zest Protocol v2
|
|
320
247
|
*/
|
|
321
248
|
async getAssets() {
|
|
322
249
|
this.ensureMainnet();
|
|
@@ -331,7 +258,6 @@ export class ZestProtocolService {
|
|
|
331
258
|
* Resolve an asset symbol or contract ID to a full contract ID
|
|
332
259
|
*/
|
|
333
260
|
async resolveAsset(assetOrSymbol) {
|
|
334
|
-
// If it looks like a contract ID, return as-is
|
|
335
261
|
if (assetOrSymbol.includes(".")) {
|
|
336
262
|
return assetOrSymbol;
|
|
337
263
|
}
|
|
@@ -339,186 +265,239 @@ export class ZestProtocolService {
|
|
|
339
265
|
return config.token;
|
|
340
266
|
}
|
|
341
267
|
/**
|
|
342
|
-
* Get user's
|
|
343
|
-
*
|
|
344
|
-
* Supply positions are tracked as LP token balances (e.g. zsbtc-v2-0.get-balance),
|
|
345
|
-
* not in pool-0-reserve-v2-0.get-user-reserve-data which only tracks borrow-side debt.
|
|
346
|
-
* Borrow positions are read from get-user-reserve-data.principal-borrow-balance.
|
|
268
|
+
* Get user's full position on Zest v2 via the data helper contract.
|
|
269
|
+
* Returns collateral, debt, health factor, and LTV data in a single call.
|
|
347
270
|
*/
|
|
348
271
|
async getUserPosition(asset, userAddress) {
|
|
349
272
|
this.ensureMainnet();
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
try {
|
|
356
|
-
const lpResult = await this.hiro.callReadOnlyFunction(assetConfig.lpToken, "get-balance", [principalCV(userAddress)], userAddress);
|
|
357
|
-
if (lpResult.okay && lpResult.result) {
|
|
358
|
-
const lpDecoded = cvToJSON(hexToCV(lpResult.result));
|
|
359
|
-
// get-balance returns (response uint uint) — success value is the balance
|
|
360
|
-
if (lpDecoded?.success && lpDecoded.value?.value !== undefined) {
|
|
361
|
-
supplied = lpDecoded.value.value;
|
|
362
|
-
}
|
|
363
|
-
else if (lpDecoded?.value !== undefined && typeof lpDecoded.value === "string") {
|
|
364
|
-
supplied = lpDecoded.value;
|
|
365
|
-
}
|
|
366
|
-
}
|
|
273
|
+
const assetConfig = this.getAssetConfig(asset);
|
|
274
|
+
try {
|
|
275
|
+
const result = await this.hiro.callReadOnlyFunction(this.contracts.data, "get-user-position", [principalCV(userAddress)], userAddress);
|
|
276
|
+
if (!result.okay || !result.result) {
|
|
277
|
+
return null;
|
|
367
278
|
}
|
|
368
|
-
|
|
369
|
-
|
|
279
|
+
const decoded = cvToJSON(hexToCV(result.result));
|
|
280
|
+
if (!decoded || decoded.success === false) {
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
// Unwrap (ok ...) → tuple with nested .value from cvToJSON
|
|
284
|
+
const position = decoded.value?.value ?? decoded.value;
|
|
285
|
+
if (!position) {
|
|
286
|
+
return null;
|
|
370
287
|
}
|
|
288
|
+
// Extract collateral shares for this asset from collateral list
|
|
289
|
+
// collateral: list of { aid: uint, amount: uint }
|
|
290
|
+
// Collateral uses zToken IDs (assetId + 1): zSTX=1, zsBTC=3, zstSTX=5, etc.
|
|
291
|
+
const zTokenId = assetConfig.assetId + 1;
|
|
292
|
+
const collateralList = position["collateral"]?.value ?? [];
|
|
293
|
+
const collateralEntry = collateralList.find((c) => String(c.value?.aid?.value) === String(zTokenId));
|
|
294
|
+
const suppliedShares = collateralEntry?.value?.amount?.value ?? "0";
|
|
295
|
+
// Extract debt for this asset from debt list
|
|
296
|
+
// debt: list of { actual-debt: uint, asset-id: uint, ... }
|
|
297
|
+
const debtList = position["debt"]?.value ?? [];
|
|
298
|
+
const debtEntry = debtList.find((d) => String(d.value?.["asset-id"]?.value) === String(assetConfig.assetId));
|
|
299
|
+
const borrowed = debtEntry?.value?.["actual-debt"]?.value ?? "0";
|
|
300
|
+
return {
|
|
301
|
+
asset,
|
|
302
|
+
suppliedShares,
|
|
303
|
+
borrowed,
|
|
304
|
+
healthFactor: position["health-factor"]?.value,
|
|
305
|
+
};
|
|
371
306
|
}
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
307
|
+
catch {
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Get detailed per-asset supply balances via the data helper.
|
|
313
|
+
* Returns vault share balances, underlying equivalents, and market collateral.
|
|
314
|
+
*/
|
|
315
|
+
async getUserSupplies(userAddress) {
|
|
316
|
+
this.ensureMainnet();
|
|
317
|
+
try {
|
|
318
|
+
const result = await this.hiro.callReadOnlyFunction(this.contracts.data, "get-supplies-user", [principalCV(userAddress)], userAddress);
|
|
319
|
+
if (!result.okay || !result.result) {
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
return cvToJSON(hexToCV(result.result));
|
|
323
|
+
}
|
|
324
|
+
catch {
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Build post-conditions for a principal sending tokens.
|
|
330
|
+
* Handles wSTX (native STX transfers) vs FT transfers.
|
|
331
|
+
*/
|
|
332
|
+
buildSendPC(principal, amount, assetConfig, mode) {
|
|
333
|
+
if (assetConfig.isNativeStx) {
|
|
334
|
+
return mode === "eq"
|
|
335
|
+
? Pc.principal(principal).willSendEq(amount).ustx()
|
|
336
|
+
: Pc.principal(principal).willSendLte(amount).ustx();
|
|
337
|
+
}
|
|
338
|
+
const builder = mode === "eq"
|
|
339
|
+
? Pc.principal(principal).willSendEq(amount)
|
|
340
|
+
: Pc.principal(principal).willSendLte(amount);
|
|
341
|
+
return builder.ft(assetConfig.token, assetConfig.tokenAssetName);
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Query the vault's convert-to-assets to predict underlying amount for a given share amount.
|
|
345
|
+
* Used to set accurate post-conditions for withdraw operations (shares appreciate over time).
|
|
346
|
+
*/
|
|
347
|
+
async getExpectedUnderlying(assetConfig, shares, senderAddress) {
|
|
348
|
+
try {
|
|
349
|
+
const result = await this.hiro.callReadOnlyFunction(assetConfig.vault, "convert-to-assets", [uintCV(shares)], senderAddress);
|
|
350
|
+
if (result.okay && result.result) {
|
|
351
|
+
const decoded = cvToJSON(hexToCV(result.result));
|
|
352
|
+
const value = decoded?.value?.value ?? decoded?.value;
|
|
353
|
+
if (value !== undefined) {
|
|
354
|
+
return BigInt(value);
|
|
385
355
|
}
|
|
386
356
|
}
|
|
387
|
-
|
|
388
|
-
|
|
357
|
+
}
|
|
358
|
+
catch {
|
|
359
|
+
// Fall back to shares as lower bound estimate
|
|
360
|
+
}
|
|
361
|
+
return shares;
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Query the vault's convert-to-shares to predict zToken amount for a given underlying amount.
|
|
365
|
+
* Used to set accurate post-conditions for supply operations.
|
|
366
|
+
*/
|
|
367
|
+
async getExpectedShares(assetConfig, amount, senderAddress) {
|
|
368
|
+
try {
|
|
369
|
+
const result = await this.hiro.callReadOnlyFunction(assetConfig.vault, "convert-to-shares", [uintCV(amount)], senderAddress);
|
|
370
|
+
if (result.okay && result.result) {
|
|
371
|
+
const decoded = cvToJSON(hexToCV(result.result));
|
|
372
|
+
const value = decoded?.value?.value ?? decoded?.value;
|
|
373
|
+
if (value !== undefined) {
|
|
374
|
+
return BigInt(value);
|
|
375
|
+
}
|
|
389
376
|
}
|
|
390
|
-
// Return null only if both reads produced nothing useful and asset config is unknown
|
|
391
|
-
if (!assetConfig && supplied === "0" && borrowed === "0") {
|
|
392
|
-
return null;
|
|
393
377
|
}
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
};
|
|
378
|
+
catch {
|
|
379
|
+
// Fall back to amount as upper bound
|
|
380
|
+
}
|
|
381
|
+
return amount;
|
|
399
382
|
}
|
|
400
383
|
/**
|
|
401
|
-
* Supply assets to Zest
|
|
384
|
+
* Supply assets to Zest v2 via market's supply-collateral-add.
|
|
385
|
+
* Atomically deposits into vault and adds zTokens as collateral.
|
|
386
|
+
* This earns yield AND provides borrowing power.
|
|
387
|
+
*
|
|
388
|
+
* Token flow (3 ft-transfers):
|
|
389
|
+
* 1. user → market (underlying)
|
|
390
|
+
* 2. market → vault (underlying)
|
|
391
|
+
* 3. user → market-vault (zTokens, minted to user then transferred)
|
|
402
392
|
*
|
|
403
|
-
* Contract
|
|
393
|
+
* Contract: v0-4-market.supply-collateral-add(ft, amount, min-shares, price-feeds)
|
|
404
394
|
*/
|
|
405
|
-
async supply(account, asset, amount
|
|
395
|
+
async supply(account, asset, amount) {
|
|
406
396
|
this.ensureMainnet();
|
|
407
397
|
const assetConfig = this.getAssetConfig(asset);
|
|
408
|
-
const { address, name } = parseContractId(this.contracts.
|
|
409
|
-
const [lpAddr, lpName] = parseContractIdTuple(assetConfig.lpToken);
|
|
398
|
+
const { address, name } = parseContractId(this.contracts.market);
|
|
410
399
|
const [assetAddr, assetName] = parseContractIdTuple(assetConfig.token);
|
|
411
|
-
|
|
400
|
+
// Pre-query expected zToken shares for accurate post-conditions
|
|
401
|
+
const expectedShares = await this.getExpectedShares(assetConfig, amount, account.address);
|
|
412
402
|
const functionArgs = [
|
|
413
|
-
contractPrincipalCV(
|
|
414
|
-
principalCV(this.contracts.poolReserve), // pool-reserve
|
|
415
|
-
contractPrincipalCV(assetAddr, assetName), // asset
|
|
403
|
+
contractPrincipalCV(assetAddr, assetName), // ft (underlying token)
|
|
416
404
|
uintCV(amount), // amount
|
|
417
|
-
|
|
418
|
-
noneCV(), //
|
|
419
|
-
contractPrincipalCV(incentivesAddr, incentivesName), // incentives
|
|
405
|
+
uintCV(expectedShares > 0n ? (expectedShares * 95n) / 100n : 0n), // min-shares (5% slippage tolerance)
|
|
406
|
+
noneCV(), // price-feeds (use cached)
|
|
420
407
|
];
|
|
421
|
-
// Post-
|
|
408
|
+
// Post-conditions for all 3 ft-transfers:
|
|
409
|
+
// 1. User sends underlying → market
|
|
410
|
+
// 2. Market forwards underlying → vault
|
|
411
|
+
// 3. User sends minted zTokens → market-vault (as collateral)
|
|
422
412
|
const postConditions = [
|
|
413
|
+
this.buildSendPC(account.address, amount, assetConfig, "eq"),
|
|
414
|
+
this.buildSendPC(ZEST_V2_MARKET, amount, assetConfig, "lte"),
|
|
423
415
|
Pc.principal(account.address)
|
|
424
|
-
.
|
|
425
|
-
.ft(assetConfig.
|
|
416
|
+
.willSendLte(expectedShares)
|
|
417
|
+
.ft(assetConfig.vault, "zft"),
|
|
426
418
|
];
|
|
427
419
|
return callContract(account, {
|
|
428
420
|
contractAddress: address,
|
|
429
421
|
contractName: name,
|
|
430
|
-
functionName: "supply",
|
|
422
|
+
functionName: "supply-collateral-add",
|
|
431
423
|
functionArgs,
|
|
432
424
|
postConditionMode: PostConditionMode.Deny,
|
|
433
425
|
postConditions,
|
|
434
426
|
});
|
|
435
427
|
}
|
|
436
428
|
/**
|
|
437
|
-
* Withdraw assets from Zest
|
|
429
|
+
* Withdraw assets from Zest v2 via market's collateral-remove-redeem.
|
|
430
|
+
* Atomically removes zToken collateral and redeems for underlying.
|
|
431
|
+
*
|
|
432
|
+
* Token flow (3 ft-transfers):
|
|
433
|
+
* 1. market-vault → market (zTokens released from collateral)
|
|
434
|
+
* 2. market → vault (zTokens for redemption/burn)
|
|
435
|
+
* 3. vault → user (underlying redeemed)
|
|
438
436
|
*
|
|
439
|
-
* Contract
|
|
437
|
+
* Contract: v0-4-market.collateral-remove-redeem(ft, amount, min-underlying, receiver, price-feeds)
|
|
438
|
+
*
|
|
439
|
+
* @param amount - Amount in zToken shares to withdraw
|
|
440
440
|
*/
|
|
441
441
|
async withdraw(account, asset, amount) {
|
|
442
442
|
this.ensureMainnet();
|
|
443
443
|
const assetConfig = this.getAssetConfig(asset);
|
|
444
|
-
const { address, name } = parseContractId(this.contracts.
|
|
445
|
-
const [
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
const
|
|
449
|
-
const priceFeedBytes = await this.fetchPriceFeedBytes();
|
|
444
|
+
const { address, name } = parseContractId(this.contracts.market);
|
|
445
|
+
const [vaultAddr, vaultName] = parseContractIdTuple(assetConfig.vault);
|
|
446
|
+
// Pre-query: how much underlying will we get for these shares?
|
|
447
|
+
// Shares appreciate over time, so underlying > shares amount.
|
|
448
|
+
const expectedUnderlying = await this.getExpectedUnderlying(assetConfig, amount, account.address);
|
|
450
449
|
const functionArgs = [
|
|
451
|
-
contractPrincipalCV(
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
principalCV(account.address), // owner
|
|
457
|
-
this.buildAssetsListCV(), // assets
|
|
458
|
-
contractPrincipalCV(incentivesAddr, incentivesName), // incentives
|
|
459
|
-
priceFeedBytes, // price-feed-bytes (Pyth VAA)
|
|
450
|
+
contractPrincipalCV(vaultAddr, vaultName), // ft (zToken / vault contract, NOT underlying)
|
|
451
|
+
uintCV(amount), // amount (zToken shares)
|
|
452
|
+
uintCV(expectedUnderlying > 0n ? (expectedUnderlying * 95n) / 100n : 0n), // min-underlying (5% slippage tolerance)
|
|
453
|
+
noneCV(), // receiver (none = tx-sender)
|
|
454
|
+
noneCV(), // price-feeds (use cached)
|
|
460
455
|
];
|
|
461
|
-
// Post-conditions:
|
|
462
|
-
// 1.
|
|
463
|
-
// 2.
|
|
464
|
-
// 3.
|
|
465
|
-
// LP tokens are minted 1:1 with supplied amount, so burning ≤ withdraw amount is safe.
|
|
466
|
-
const [lpFtContract, lpFtAssetName] = assetConfig.lpFungibleToken.split("::");
|
|
456
|
+
// Post-conditions (Deny mode requires ALL ft-transfers to be covered):
|
|
457
|
+
// 1. market-vault transfers zTokens (collateral release)
|
|
458
|
+
// 2. market transfers zTokens (internal accounting)
|
|
459
|
+
// 3. vault sends underlying → user (redemption, amount = convert-to-assets result)
|
|
467
460
|
const postConditions = [
|
|
468
|
-
Pc.principal(
|
|
461
|
+
Pc.principal(ZEST_V2_MARKET_VAULT)
|
|
469
462
|
.willSendLte(amount)
|
|
470
|
-
.ft(assetConfig.
|
|
471
|
-
Pc.principal(
|
|
472
|
-
.willSendLte(100n)
|
|
473
|
-
.ustx(),
|
|
474
|
-
Pc.principal(account.address)
|
|
463
|
+
.ft(assetConfig.vault, "zft"),
|
|
464
|
+
Pc.principal(ZEST_V2_MARKET)
|
|
475
465
|
.willSendLte(amount)
|
|
476
|
-
.ft(
|
|
466
|
+
.ft(assetConfig.vault, "zft"),
|
|
467
|
+
this.buildSendPC(assetConfig.vault, expectedUnderlying, assetConfig, "lte"),
|
|
477
468
|
];
|
|
478
469
|
return callContract(account, {
|
|
479
470
|
contractAddress: address,
|
|
480
471
|
contractName: name,
|
|
481
|
-
functionName: "
|
|
472
|
+
functionName: "collateral-remove-redeem",
|
|
482
473
|
functionArgs,
|
|
483
474
|
postConditionMode: PostConditionMode.Deny,
|
|
484
475
|
postConditions,
|
|
485
476
|
});
|
|
486
477
|
}
|
|
487
478
|
/**
|
|
488
|
-
* Borrow assets from Zest
|
|
479
|
+
* Borrow assets from Zest v2 via market's borrow function.
|
|
480
|
+
* Requires sufficient collateral to maintain healthy LTV.
|
|
481
|
+
*
|
|
482
|
+
* Token flow (1 ft-transfer):
|
|
483
|
+
* 1. vault → user (borrowed underlying)
|
|
489
484
|
*
|
|
490
|
-
* Contract
|
|
485
|
+
* Contract: v0-4-market.borrow(ft, amount, receiver, price-feeds)
|
|
491
486
|
*/
|
|
492
487
|
async borrow(account, asset, amount) {
|
|
493
488
|
this.ensureMainnet();
|
|
494
489
|
const assetConfig = this.getAssetConfig(asset);
|
|
495
|
-
const { address, name } = parseContractId(this.contracts.
|
|
490
|
+
const { address, name } = parseContractId(this.contracts.market);
|
|
496
491
|
const [assetAddr, assetName] = parseContractIdTuple(assetConfig.token);
|
|
497
|
-
const [lpAddr, lpName] = parseContractIdTuple(assetConfig.lpToken);
|
|
498
|
-
const [oracleAddr, oracleName] = parseContractIdTuple(assetConfig.oracle);
|
|
499
|
-
const priceFeedBytes = await this.fetchPriceFeedBytes();
|
|
500
492
|
const functionArgs = [
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
this.buildAssetsListCV(), // assets
|
|
506
|
-
uintCV(amount), // amount-to-be-borrowed
|
|
507
|
-
principalCV(this.contracts.feesCalculator), // fee-calculator
|
|
508
|
-
uintCV(BigInt(0)), // interest-rate-mode (0 = variable)
|
|
509
|
-
principalCV(account.address), // owner
|
|
510
|
-
priceFeedBytes, // price-feed-bytes (Pyth VAA)
|
|
493
|
+
contractPrincipalCV(assetAddr, assetName), // ft (token to borrow)
|
|
494
|
+
uintCV(amount), // amount
|
|
495
|
+
noneCV(), // receiver (none = tx-sender)
|
|
496
|
+
noneCV(), // price-feeds (use cached)
|
|
511
497
|
];
|
|
512
|
-
// Post-
|
|
513
|
-
// 1. pool-vault sends borrowed asset (not pool-reserve)
|
|
514
|
-
// 2. sender pays small STX fee for Pyth oracle update (~2 uSTX)
|
|
498
|
+
// Post-condition: vault sends borrowed underlying to user
|
|
515
499
|
const postConditions = [
|
|
516
|
-
|
|
517
|
-
.willSendLte(amount)
|
|
518
|
-
.ft(assetConfig.token, assetName),
|
|
519
|
-
Pc.principal(account.address)
|
|
520
|
-
.willSendLte(100n)
|
|
521
|
-
.ustx(),
|
|
500
|
+
this.buildSendPC(assetConfig.vault, amount, assetConfig, "lte"),
|
|
522
501
|
];
|
|
523
502
|
return callContract(account, {
|
|
524
503
|
contractAddress: address,
|
|
@@ -530,26 +509,26 @@ export class ZestProtocolService {
|
|
|
530
509
|
});
|
|
531
510
|
}
|
|
532
511
|
/**
|
|
533
|
-
* Repay borrowed assets
|
|
512
|
+
* Repay borrowed assets on Zest v2.
|
|
534
513
|
*
|
|
535
|
-
*
|
|
514
|
+
* Token flow (1 ft-transfer):
|
|
515
|
+
* 1. user → vault (repayment, amount may be capped to actual debt on-chain)
|
|
516
|
+
*
|
|
517
|
+
* Contract: v0-4-market.repay(ft, amount, on-behalf-of)
|
|
536
518
|
*/
|
|
537
519
|
async repay(account, asset, amount, onBehalfOf) {
|
|
538
520
|
this.ensureMainnet();
|
|
539
521
|
const assetConfig = this.getAssetConfig(asset);
|
|
540
|
-
const { address, name } = parseContractId(this.contracts.
|
|
522
|
+
const { address, name } = parseContractId(this.contracts.market);
|
|
541
523
|
const [assetAddr, assetName] = parseContractIdTuple(assetConfig.token);
|
|
542
524
|
const functionArgs = [
|
|
543
|
-
contractPrincipalCV(assetAddr, assetName), //
|
|
544
|
-
uintCV(amount), // amount
|
|
545
|
-
principalCV(onBehalfOf
|
|
546
|
-
principalCV(account.address), // payer
|
|
525
|
+
contractPrincipalCV(assetAddr, assetName), // ft
|
|
526
|
+
uintCV(amount), // amount
|
|
527
|
+
onBehalfOf ? someCV(principalCV(onBehalfOf)) : noneCV(), // on-behalf-of
|
|
547
528
|
];
|
|
548
|
-
// Post-condition: user
|
|
529
|
+
// Post-condition: user sends repayment (use lte since contract may cap to actual debt)
|
|
549
530
|
const postConditions = [
|
|
550
|
-
|
|
551
|
-
.willSendLte(amount)
|
|
552
|
-
.ft(assetConfig.token, assetName),
|
|
531
|
+
this.buildSendPC(account.address, amount, assetConfig, "lte"),
|
|
553
532
|
];
|
|
554
533
|
return callContract(account, {
|
|
555
534
|
contractAddress: address,
|
|
@@ -561,74 +540,69 @@ export class ZestProtocolService {
|
|
|
561
540
|
});
|
|
562
541
|
}
|
|
563
542
|
/**
|
|
564
|
-
*
|
|
543
|
+
* Deposit directly into a Zest v2 vault for yield without collateral.
|
|
544
|
+
* Mints zTokens that earn supply yield. Simpler than supply-collateral-add
|
|
545
|
+
* but the zTokens won't be usable as collateral for borrowing.
|
|
565
546
|
*
|
|
566
|
-
*
|
|
547
|
+
* Token flow (1 ft-transfer):
|
|
548
|
+
* 1. user → vault (underlying)
|
|
549
|
+
* Note: zTokens minted to recipient (ft-mint, no PC needed)
|
|
567
550
|
*
|
|
568
|
-
* Contract
|
|
551
|
+
* Contract: vault.deposit(amount, min-out, recipient)
|
|
569
552
|
*/
|
|
570
|
-
async
|
|
553
|
+
async depositToVault(account, asset, amount) {
|
|
571
554
|
this.ensureMainnet();
|
|
572
555
|
const assetConfig = this.getAssetConfig(asset);
|
|
573
|
-
const { address, name } = parseContractId(
|
|
574
|
-
const [lpAddr, lpName] = parseContractIdTuple(assetConfig.lpToken);
|
|
575
|
-
const [assetAddr, assetName] = parseContractIdTuple(assetConfig.token);
|
|
576
|
-
const [oracleAddr, oracleName] = parseContractIdTuple(assetConfig.oracle);
|
|
577
|
-
const [incentivesAddr, incentivesName] = parseContractIdTuple(this.contracts.incentives);
|
|
578
|
-
const [wstxAddr, wstxName] = parseContractIdTuple(this.contracts.wstx);
|
|
579
|
-
// Pre-check: query pending rewards before broadcasting to avoid wasting gas
|
|
580
|
-
// when there are no rewards (on-chain tx would abort with ERR_NO_REWARDS)
|
|
581
|
-
const rewardsResult = await this.hiro.callReadOnlyFunction(this.contracts.incentives, "get-vault-rewards", [
|
|
582
|
-
principalCV(account.address),
|
|
583
|
-
contractPrincipalCV(assetAddr, assetName),
|
|
584
|
-
contractPrincipalCV(wstxAddr, wstxName),
|
|
585
|
-
], account.address);
|
|
586
|
-
if (rewardsResult.okay && rewardsResult.result) {
|
|
587
|
-
const decoded = cvToJSON(hexToCV(rewardsResult.result));
|
|
588
|
-
// get-vault-rewards returns a bare uint, but handle (ok uint) / (response uint uint)
|
|
589
|
-
// defensively in case the contract is upgraded.
|
|
590
|
-
// Bare uint: { type: "uint", value: "123" }
|
|
591
|
-
// Response-wrapped: { type: "ok", value: { type: "uint", value: "123" } }
|
|
592
|
-
const rawValue = typeof decoded?.value === "object" && decoded.value?.value !== undefined
|
|
593
|
-
? decoded.value.value
|
|
594
|
-
: decoded?.value;
|
|
595
|
-
if (rawValue === undefined) {
|
|
596
|
-
// Can't decode response -- skip pre-check, let the on-chain tx decide
|
|
597
|
-
}
|
|
598
|
-
else if (BigInt(rawValue) === 0n) {
|
|
599
|
-
throw new Error("No rewards available to claim. Skipping broadcast to avoid wasting gas.");
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
else if (!rewardsResult.okay) {
|
|
603
|
-
console.error(`[zest] get-vault-rewards read-only call failed: ${rewardsResult.result ?? "unknown error"}. Skipping pre-check.`);
|
|
604
|
-
}
|
|
605
|
-
const priceFeedBytes = await this.fetchPriceFeedBytes();
|
|
556
|
+
const { address, name } = parseContractId(assetConfig.vault);
|
|
606
557
|
const functionArgs = [
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
558
|
+
uintCV(amount), // amount
|
|
559
|
+
uintCV(0n), // min-out (0 = no slippage protection)
|
|
560
|
+
principalCV(account.address), // recipient
|
|
561
|
+
];
|
|
562
|
+
// Post-condition: user sends underlying token to vault
|
|
563
|
+
const postConditions = [
|
|
564
|
+
this.buildSendPC(account.address, amount, assetConfig, "eq"),
|
|
565
|
+
];
|
|
566
|
+
return callContract(account, {
|
|
567
|
+
contractAddress: address,
|
|
568
|
+
contractName: name,
|
|
569
|
+
functionName: "deposit",
|
|
570
|
+
functionArgs,
|
|
571
|
+
postConditionMode: PostConditionMode.Deny,
|
|
572
|
+
postConditions,
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
576
|
+
* Redeem zTokens from a Zest v2 vault for underlying assets.
|
|
577
|
+
*
|
|
578
|
+
* Token flow:
|
|
579
|
+
* 1. user sends zTokens → vault (ft-burn, but transfer happens first)
|
|
580
|
+
* 2. vault sends underlying → recipient
|
|
581
|
+
*
|
|
582
|
+
* Contract: vault.redeem(amount, min-out, recipient)
|
|
583
|
+
*
|
|
584
|
+
* @param amount - Amount of zTokens to redeem
|
|
585
|
+
*/
|
|
586
|
+
async redeemFromVault(account, asset, amount) {
|
|
587
|
+
this.ensureMainnet();
|
|
588
|
+
const assetConfig = this.getAssetConfig(asset);
|
|
589
|
+
const { address, name } = parseContractId(assetConfig.vault);
|
|
590
|
+
// Pre-query: shares → underlying (shares appreciate, so underlying > shares)
|
|
591
|
+
const expectedUnderlying = await this.getExpectedUnderlying(assetConfig, amount, account.address);
|
|
592
|
+
const functionArgs = [
|
|
593
|
+
uintCV(amount), // amount (zToken shares)
|
|
594
|
+
uintCV(0n), // min-out (0 = no slippage protection)
|
|
595
|
+
principalCV(account.address), // recipient
|
|
616
596
|
];
|
|
617
597
|
// Post-conditions:
|
|
618
|
-
// 1.
|
|
619
|
-
// 2. sender pays small STX fee for Pyth oracle update (~2 uSTX)
|
|
598
|
+
// 1. Vault sends underlying to user (amount from convert-to-assets)
|
|
620
599
|
const postConditions = [
|
|
621
|
-
|
|
622
|
-
.willSendGte(0n)
|
|
623
|
-
.ft(this.contracts.wstx, wstxName),
|
|
624
|
-
Pc.principal(account.address)
|
|
625
|
-
.willSendLte(100n)
|
|
626
|
-
.ustx(),
|
|
600
|
+
this.buildSendPC(assetConfig.vault, expectedUnderlying, assetConfig, "lte"),
|
|
627
601
|
];
|
|
628
602
|
return callContract(account, {
|
|
629
603
|
contractAddress: address,
|
|
630
604
|
contractName: name,
|
|
631
|
-
functionName: "
|
|
605
|
+
functionName: "redeem",
|
|
632
606
|
functionArgs,
|
|
633
607
|
postConditionMode: PostConditionMode.Deny,
|
|
634
608
|
postConditions,
|