@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.
Files changed (35) hide show
  1. package/dist/config/contracts.d.ts +35 -32
  2. package/dist/config/contracts.d.ts.map +1 -1
  3. package/dist/config/contracts.js +58 -102
  4. package/dist/config/contracts.js.map +1 -1
  5. package/dist/config/pillar.d.ts.map +1 -1
  6. package/dist/config/pillar.js +3 -1
  7. package/dist/config/pillar.js.map +1 -1
  8. package/dist/index.js +32 -20
  9. package/dist/index.js.map +1 -1
  10. package/dist/services/defi.service.d.ts +73 -40
  11. package/dist/services/defi.service.d.ts.map +1 -1
  12. package/dist/services/defi.service.js +237 -263
  13. package/dist/services/defi.service.js.map +1 -1
  14. package/dist/tools/defi.tools.d.ts.map +1 -1
  15. package/dist/tools/defi.tools.js +40 -80
  16. package/dist/tools/defi.tools.js.map +1 -1
  17. package/dist/tools/index.d.ts +1 -8
  18. package/dist/tools/index.d.ts.map +1 -1
  19. package/dist/tools/index.js +36 -63
  20. package/dist/tools/index.js.map +1 -1
  21. package/dist/tools/pillar-direct.tools.d.ts.map +1 -1
  22. package/dist/tools/pillar-direct.tools.js +100 -113
  23. package/dist/tools/pillar-direct.tools.js.map +1 -1
  24. package/dist/tools/styx.tools.d.ts +20 -0
  25. package/dist/tools/styx.tools.d.ts.map +1 -0
  26. package/dist/tools/styx.tools.js +401 -0
  27. package/dist/tools/styx.tools.js.map +1 -0
  28. package/dist/tools/yield-hunter.tools.d.ts.map +1 -1
  29. package/dist/tools/yield-hunter.tools.js +15 -18
  30. package/dist/tools/yield-hunter.tools.js.map +1 -1
  31. package/dist/utils/validation.d.ts +1 -1
  32. package/dist/yield-hunter/index.js +2 -2
  33. package/dist/yield-hunter/index.js.map +1 -1
  34. package/package.json +2 -1
  35. package/skill/SKILL.md +1 -1
@@ -1,8 +1,8 @@
1
- import { uintCV, contractPrincipalCV, cvToJSON, hexToCV, PostConditionMode, Pc, principalCV, broadcastTransaction, makeContractCall, listCV, tupleCV, noneCV, someCV, bufferCV, } from "@stacks/transactions";
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, ZEST_ASSETS_LIST, } from "../config/index.js";
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
- * Build the assets-list CV required for borrow/withdraw operations
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 reserve/position data for an asset.
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
- // Look up the asset config to find the LP token contract
351
- const assetConfig = Object.values(ZEST_ASSETS).find((a) => a.token === asset);
352
- // Read supply position from LP token balance
353
- let supplied = "0";
354
- if (assetConfig) {
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
- catch {
369
- // LP token read failed; leave supplied as "0"
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
- // Read borrow position from pool-borrow reserve data
373
- let borrowed = "0";
374
- if (assetConfig)
375
- try {
376
- const borrowResult = await this.hiro.callReadOnlyFunction(this.contracts.poolBorrow, "get-user-reserve-data", [
377
- principalCV(userAddress),
378
- contractPrincipalCV(...parseContractIdTuple(assetConfig.token)),
379
- ], userAddress);
380
- if (borrowResult.okay && borrowResult.result) {
381
- const borrowDecoded = cvToJSON(hexToCV(borrowResult.result));
382
- if (borrowDecoded && typeof borrowDecoded === "object") {
383
- borrowed = borrowDecoded["principal-borrow-balance"]?.value || "0";
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
- catch {
388
- // Borrow data read failed; leave borrowed as "0"
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
- return {
395
- asset,
396
- supplied,
397
- borrowed,
398
- };
378
+ catch {
379
+ // Fall back to amount as upper bound
380
+ }
381
+ return amount;
399
382
  }
400
383
  /**
401
- * Supply assets to Zest lending pool via borrow-helper
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 signature: supply(lp, pool-reserve, asset, amount, owner, referral, incentives)
393
+ * Contract: v0-4-market.supply-collateral-add(ft, amount, min-shares, price-feeds)
404
394
  */
405
- async supply(account, asset, amount, onBehalfOf) {
395
+ async supply(account, asset, amount) {
406
396
  this.ensureMainnet();
407
397
  const assetConfig = this.getAssetConfig(asset);
408
- const { address, name } = parseContractId(this.contracts.borrowHelper);
409
- const [lpAddr, lpName] = parseContractIdTuple(assetConfig.lpToken);
398
+ const { address, name } = parseContractId(this.contracts.market);
410
399
  const [assetAddr, assetName] = parseContractIdTuple(assetConfig.token);
411
- const [incentivesAddr, incentivesName] = parseContractIdTuple(this.contracts.incentives);
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(lpAddr, lpName), // lp
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
- principalCV(onBehalfOf || account.address), // owner
418
- noneCV(), // referral (none for now)
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-condition: user will send the asset
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
- .willSendEq(amount)
425
- .ft(assetConfig.token, assetName),
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 lending pool via borrow-helper
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 signature: withdraw(lp, pool-reserve, asset, oracle, amount, owner, assets, incentives, price-feed-bytes)
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.borrowHelper);
445
- const [assetAddr, assetName] = parseContractIdTuple(assetConfig.token);
446
- const [lpAddr, lpName] = parseContractIdTuple(assetConfig.lpToken);
447
- const [oracleAddr, oracleName] = parseContractIdTuple(assetConfig.oracle);
448
- const [incentivesAddr, incentivesName] = parseContractIdTuple(this.contracts.incentives);
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(lpAddr, lpName), // lp
452
- principalCV(this.contracts.poolReserve), // pool-reserve
453
- contractPrincipalCV(assetAddr, assetName), // asset
454
- contractPrincipalCV(oracleAddr, oracleName), // oracle
455
- uintCV(amount), // amount
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. pool-vault sends us the withdrawn asset (not pool-reserve)
463
- // 2. sender pays small STX fee for Pyth oracle update (~2 uSTX)
464
- // 3. sender burns LP tokens (zsbtc etc.)
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(this.contracts.poolVault)
461
+ Pc.principal(ZEST_V2_MARKET_VAULT)
469
462
  .willSendLte(amount)
470
- .ft(assetConfig.token, assetName),
471
- Pc.principal(account.address)
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(lpFtContract, lpFtAssetName),
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: "withdraw",
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 lending pool via borrow-helper
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 signature: borrow(pool-reserve, oracle, asset-to-borrow, lp, assets, amount, fee-calculator, interest-rate-mode, owner, price-feed-bytes)
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.borrowHelper);
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
- principalCV(this.contracts.poolReserve), // pool-reserve
502
- contractPrincipalCV(oracleAddr, oracleName), // oracle
503
- contractPrincipalCV(assetAddr, assetName), // asset-to-borrow
504
- contractPrincipalCV(lpAddr, lpName), // lp
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-conditions:
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
- Pc.principal(this.contracts.poolVault)
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
- * Contract signature: repay(asset, amount-to-repay, on-behalf-of, payer)
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.poolBorrow);
522
+ const { address, name } = parseContractId(this.contracts.market);
541
523
  const [assetAddr, assetName] = parseContractIdTuple(assetConfig.token);
542
524
  const functionArgs = [
543
- contractPrincipalCV(assetAddr, assetName), // asset
544
- uintCV(amount), // amount-to-repay
545
- principalCV(onBehalfOf || account.address), // on-behalf-of
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 will send the asset to repay
529
+ // Post-condition: user sends repayment (use lte since contract may cap to actual debt)
549
530
  const postConditions = [
550
- Pc.principal(account.address)
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
- * Claim accumulated rewards from Zest incentives program via borrow-helper
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
- * Currently: sBTC suppliers earn wSTX rewards
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 signature: claim-rewards(lp, pool-reserve, asset, oracle, owner, assets, reward-asset, incentives, price-feed-bytes)
551
+ * Contract: vault.deposit(amount, min-out, recipient)
569
552
  */
570
- async claimRewards(account, asset) {
553
+ async depositToVault(account, asset, amount) {
571
554
  this.ensureMainnet();
572
555
  const assetConfig = this.getAssetConfig(asset);
573
- const { address, name } = parseContractId(this.contracts.borrowHelper);
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
- contractPrincipalCV(lpAddr, lpName), // lp
608
- principalCV(this.contracts.poolReserve), // pool-reserve
609
- contractPrincipalCV(assetAddr, assetName), // asset
610
- contractPrincipalCV(oracleAddr, oracleName), // oracle
611
- principalCV(account.address), // owner
612
- this.buildAssetsListCV(), // assets
613
- contractPrincipalCV(wstxAddr, wstxName), // reward-asset (wSTX)
614
- contractPrincipalCV(incentivesAddr, incentivesName), // incentives
615
- priceFeedBytes, // price-feed-bytes (Pyth VAA)
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. pool reserve will send wSTX rewards to user
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
- Pc.principal(this.contracts.poolReserve)
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: "claim-rewards",
605
+ functionName: "redeem",
632
606
  functionArgs,
633
607
  postConditionMode: PostConditionMode.Deny,
634
608
  postConditions,