@aibtc/mcp-server 1.33.2 → 1.33.4

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.
@@ -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,185 +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 pool-borrow-v2-3.get-user-reserve-data.
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;
370
282
  }
283
+ // Unwrap (ok ...) → tuple with nested .value from cvToJSON
284
+ const position = decoded.value?.value ?? decoded.value;
285
+ if (!position) {
286
+ return null;
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
+ };
306
+ }
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));
371
323
  }
372
- // Read borrow position from pool-borrow reserve data
373
- let borrowed = "0";
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) {
374
348
  try {
375
- const borrowResult = await this.hiro.callReadOnlyFunction(this.contracts.poolBorrow, "get-user-reserve-data", [
376
- principalCV(userAddress),
377
- contractPrincipalCV(...parseContractIdTuple(asset)),
378
- ], userAddress);
379
- if (borrowResult.okay && borrowResult.result) {
380
- const borrowDecoded = cvToJSON(hexToCV(borrowResult.result));
381
- if (borrowDecoded && typeof borrowDecoded === "object") {
382
- borrowed = borrowDecoded["current-variable-debt"]?.value || "0";
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);
383
355
  }
384
356
  }
385
357
  }
386
358
  catch {
387
- // Borrow data read failed; leave borrowed as "0"
359
+ // Fall back to shares as lower bound estimate
388
360
  }
389
- // Return null only if both reads produced nothing useful and asset config is unknown
390
- if (!assetConfig && supplied === "0" && borrowed === "0") {
391
- return null;
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
+ }
376
+ }
392
377
  }
393
- return {
394
- asset,
395
- supplied,
396
- borrowed,
397
- };
378
+ catch {
379
+ // Fall back to amount as upper bound
380
+ }
381
+ return amount;
398
382
  }
399
383
  /**
400
- * 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)
401
392
  *
402
- * 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)
403
394
  */
404
- async supply(account, asset, amount, onBehalfOf) {
395
+ async supply(account, asset, amount) {
405
396
  this.ensureMainnet();
406
397
  const assetConfig = this.getAssetConfig(asset);
407
- const { address, name } = parseContractId(this.contracts.borrowHelper);
408
- const [lpAddr, lpName] = parseContractIdTuple(assetConfig.lpToken);
398
+ const { address, name } = parseContractId(this.contracts.market);
409
399
  const [assetAddr, assetName] = parseContractIdTuple(assetConfig.token);
410
- 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);
411
402
  const functionArgs = [
412
- contractPrincipalCV(lpAddr, lpName), // lp
413
- principalCV(this.contracts.poolReserve), // pool-reserve
414
- contractPrincipalCV(assetAddr, assetName), // asset
403
+ contractPrincipalCV(assetAddr, assetName), // ft (underlying token)
415
404
  uintCV(amount), // amount
416
- principalCV(onBehalfOf || account.address), // owner
417
- noneCV(), // referral (none for now)
418
- contractPrincipalCV(incentivesAddr, incentivesName), // incentives
405
+ uintCV(expectedShares > 0n ? (expectedShares * 95n) / 100n : 0n), // min-shares (5% slippage tolerance)
406
+ noneCV(), // price-feeds (use cached)
419
407
  ];
420
- // 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)
421
412
  const postConditions = [
413
+ this.buildSendPC(account.address, amount, assetConfig, "eq"),
414
+ this.buildSendPC(ZEST_V2_MARKET, amount, assetConfig, "lte"),
422
415
  Pc.principal(account.address)
423
- .willSendEq(amount)
424
- .ft(assetConfig.token, assetName),
416
+ .willSendLte(expectedShares)
417
+ .ft(assetConfig.vault, "zft"),
425
418
  ];
426
419
  return callContract(account, {
427
420
  contractAddress: address,
428
421
  contractName: name,
429
- functionName: "supply",
422
+ functionName: "supply-collateral-add",
430
423
  functionArgs,
431
424
  postConditionMode: PostConditionMode.Deny,
432
425
  postConditions,
433
426
  });
434
427
  }
435
428
  /**
436
- * 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.
437
431
  *
438
- * Contract signature: withdraw(lp, pool-reserve, asset, oracle, amount, owner, assets, incentives, price-feed-bytes)
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)
436
+ *
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
439
440
  */
440
441
  async withdraw(account, asset, amount) {
441
442
  this.ensureMainnet();
442
443
  const assetConfig = this.getAssetConfig(asset);
443
- const { address, name } = parseContractId(this.contracts.borrowHelper);
444
- const [assetAddr, assetName] = parseContractIdTuple(assetConfig.token);
445
- const [lpAddr, lpName] = parseContractIdTuple(assetConfig.lpToken);
446
- const [oracleAddr, oracleName] = parseContractIdTuple(assetConfig.oracle);
447
- const [incentivesAddr, incentivesName] = parseContractIdTuple(this.contracts.incentives);
448
- 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);
449
449
  const functionArgs = [
450
- contractPrincipalCV(lpAddr, lpName), // lp
451
- principalCV(this.contracts.poolReserve), // pool-reserve
452
- contractPrincipalCV(assetAddr, assetName), // asset
453
- contractPrincipalCV(oracleAddr, oracleName), // oracle
454
- uintCV(amount), // amount
455
- principalCV(account.address), // owner
456
- this.buildAssetsListCV(), // assets
457
- contractPrincipalCV(incentivesAddr, incentivesName), // incentives
458
- 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)
459
455
  ];
460
- // Post-conditions:
461
- // 1. pool-vault sends us the withdrawn asset (not pool-reserve)
462
- // 2. sender pays small STX fee for Pyth oracle update (~2 uSTX)
463
- // 3. sender burns LP tokens (zsbtc etc.)
464
- // LP tokens are minted 1:1 with supplied amount, so burning ≤ withdraw amount is safe.
465
- 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)
466
460
  const postConditions = [
467
- Pc.principal(this.contracts.poolVault)
461
+ Pc.principal(ZEST_V2_MARKET_VAULT)
468
462
  .willSendLte(amount)
469
- .ft(assetConfig.token, assetName),
470
- Pc.principal(account.address)
471
- .willSendLte(100n)
472
- .ustx(),
473
- Pc.principal(account.address)
463
+ .ft(assetConfig.vault, "zft"),
464
+ Pc.principal(ZEST_V2_MARKET)
474
465
  .willSendLte(amount)
475
- .ft(lpFtContract, lpFtAssetName),
466
+ .ft(assetConfig.vault, "zft"),
467
+ this.buildSendPC(assetConfig.vault, expectedUnderlying, assetConfig, "lte"),
476
468
  ];
477
469
  return callContract(account, {
478
470
  contractAddress: address,
479
471
  contractName: name,
480
- functionName: "withdraw",
472
+ functionName: "collateral-remove-redeem",
481
473
  functionArgs,
482
474
  postConditionMode: PostConditionMode.Deny,
483
475
  postConditions,
484
476
  });
485
477
  }
486
478
  /**
487
- * 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.
488
481
  *
489
- * Contract signature: borrow(pool-reserve, oracle, asset-to-borrow, lp, assets, amount, fee-calculator, interest-rate-mode, owner, price-feed-bytes)
482
+ * Token flow (1 ft-transfer):
483
+ * 1. vault → user (borrowed underlying)
484
+ *
485
+ * Contract: v0-4-market.borrow(ft, amount, receiver, price-feeds)
490
486
  */
491
487
  async borrow(account, asset, amount) {
492
488
  this.ensureMainnet();
493
489
  const assetConfig = this.getAssetConfig(asset);
494
- const { address, name } = parseContractId(this.contracts.borrowHelper);
490
+ const { address, name } = parseContractId(this.contracts.market);
495
491
  const [assetAddr, assetName] = parseContractIdTuple(assetConfig.token);
496
- const [lpAddr, lpName] = parseContractIdTuple(assetConfig.lpToken);
497
- const [oracleAddr, oracleName] = parseContractIdTuple(assetConfig.oracle);
498
- const priceFeedBytes = await this.fetchPriceFeedBytes();
499
492
  const functionArgs = [
500
- principalCV(this.contracts.poolReserve), // pool-reserve
501
- contractPrincipalCV(oracleAddr, oracleName), // oracle
502
- contractPrincipalCV(assetAddr, assetName), // asset-to-borrow
503
- contractPrincipalCV(lpAddr, lpName), // lp
504
- this.buildAssetsListCV(), // assets
505
- uintCV(amount), // amount-to-be-borrowed
506
- principalCV(this.contracts.feesCalculator), // fee-calculator
507
- uintCV(BigInt(0)), // interest-rate-mode (0 = variable)
508
- principalCV(account.address), // owner
509
- 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)
510
497
  ];
511
- // Post-conditions:
512
- // 1. pool-vault sends borrowed asset (not pool-reserve)
513
- // 2. sender pays small STX fee for Pyth oracle update (~2 uSTX)
498
+ // Post-condition: vault sends borrowed underlying to user
514
499
  const postConditions = [
515
- Pc.principal(this.contracts.poolVault)
516
- .willSendLte(amount)
517
- .ft(assetConfig.token, assetName),
518
- Pc.principal(account.address)
519
- .willSendLte(100n)
520
- .ustx(),
500
+ this.buildSendPC(assetConfig.vault, amount, assetConfig, "lte"),
521
501
  ];
522
502
  return callContract(account, {
523
503
  contractAddress: address,
@@ -529,26 +509,26 @@ export class ZestProtocolService {
529
509
  });
530
510
  }
531
511
  /**
532
- * Repay borrowed assets
512
+ * Repay borrowed assets on Zest v2.
513
+ *
514
+ * Token flow (1 ft-transfer):
515
+ * 1. user → vault (repayment, amount may be capped to actual debt on-chain)
533
516
  *
534
- * Contract signature: repay(asset, amount-to-repay, on-behalf-of, payer)
517
+ * Contract: v0-4-market.repay(ft, amount, on-behalf-of)
535
518
  */
536
519
  async repay(account, asset, amount, onBehalfOf) {
537
520
  this.ensureMainnet();
538
521
  const assetConfig = this.getAssetConfig(asset);
539
- const { address, name } = parseContractId(this.contracts.poolBorrow);
522
+ const { address, name } = parseContractId(this.contracts.market);
540
523
  const [assetAddr, assetName] = parseContractIdTuple(assetConfig.token);
541
524
  const functionArgs = [
542
- contractPrincipalCV(assetAddr, assetName), // asset
543
- uintCV(amount), // amount-to-repay
544
- principalCV(onBehalfOf || account.address), // on-behalf-of
545
- principalCV(account.address), // payer
525
+ contractPrincipalCV(assetAddr, assetName), // ft
526
+ uintCV(amount), // amount
527
+ onBehalfOf ? someCV(principalCV(onBehalfOf)) : noneCV(), // on-behalf-of
546
528
  ];
547
- // Post-condition: user will send the asset to repay
529
+ // Post-condition: user sends repayment (use lte since contract may cap to actual debt)
548
530
  const postConditions = [
549
- Pc.principal(account.address)
550
- .willSendLte(amount)
551
- .ft(assetConfig.token, assetName),
531
+ this.buildSendPC(account.address, amount, assetConfig, "lte"),
552
532
  ];
553
533
  return callContract(account, {
554
534
  contractAddress: address,
@@ -560,74 +540,69 @@ export class ZestProtocolService {
560
540
  });
561
541
  }
562
542
  /**
563
- * 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.
564
546
  *
565
- * 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)
566
550
  *
567
- * 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)
568
552
  */
569
- async claimRewards(account, asset) {
553
+ async depositToVault(account, asset, amount) {
570
554
  this.ensureMainnet();
571
555
  const assetConfig = this.getAssetConfig(asset);
572
- const { address, name } = parseContractId(this.contracts.borrowHelper);
573
- const [lpAddr, lpName] = parseContractIdTuple(assetConfig.lpToken);
574
- const [assetAddr, assetName] = parseContractIdTuple(assetConfig.token);
575
- const [oracleAddr, oracleName] = parseContractIdTuple(assetConfig.oracle);
576
- const [incentivesAddr, incentivesName] = parseContractIdTuple(this.contracts.incentives);
577
- const [wstxAddr, wstxName] = parseContractIdTuple(this.contracts.wstx);
578
- // Pre-check: query pending rewards before broadcasting to avoid wasting gas
579
- // when there are no rewards (on-chain tx would abort with ERR_NO_REWARDS)
580
- const rewardsResult = await this.hiro.callReadOnlyFunction(this.contracts.incentives, "get-vault-rewards", [
581
- principalCV(account.address),
582
- contractPrincipalCV(assetAddr, assetName),
583
- contractPrincipalCV(wstxAddr, wstxName),
584
- ], account.address);
585
- if (rewardsResult.okay && rewardsResult.result) {
586
- const decoded = cvToJSON(hexToCV(rewardsResult.result));
587
- // get-vault-rewards returns a bare uint, but handle (ok uint) / (response uint uint)
588
- // defensively in case the contract is upgraded.
589
- // Bare uint: { type: "uint", value: "123" }
590
- // Response-wrapped: { type: "ok", value: { type: "uint", value: "123" } }
591
- const rawValue = typeof decoded?.value === "object" && decoded.value?.value !== undefined
592
- ? decoded.value.value
593
- : decoded?.value;
594
- if (rawValue === undefined) {
595
- // Can't decode response -- skip pre-check, let the on-chain tx decide
596
- }
597
- else if (BigInt(rawValue) === 0n) {
598
- throw new Error("No rewards available to claim. Skipping broadcast to avoid wasting gas.");
599
- }
600
- }
601
- else if (!rewardsResult.okay) {
602
- console.error(`[zest] get-vault-rewards read-only call failed: ${rewardsResult.result ?? "unknown error"}. Skipping pre-check.`);
603
- }
604
- const priceFeedBytes = await this.fetchPriceFeedBytes();
556
+ const { address, name } = parseContractId(assetConfig.vault);
557
+ const functionArgs = [
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);
605
592
  const functionArgs = [
606
- contractPrincipalCV(lpAddr, lpName), // lp
607
- principalCV(this.contracts.poolReserve), // pool-reserve
608
- contractPrincipalCV(assetAddr, assetName), // asset
609
- contractPrincipalCV(oracleAddr, oracleName), // oracle
610
- principalCV(account.address), // owner
611
- this.buildAssetsListCV(), // assets
612
- contractPrincipalCV(wstxAddr, wstxName), // reward-asset (wSTX)
613
- contractPrincipalCV(incentivesAddr, incentivesName), // incentives
614
- priceFeedBytes, // price-feed-bytes (Pyth VAA)
593
+ uintCV(amount), // amount (zToken shares)
594
+ uintCV(0n), // min-out (0 = no slippage protection)
595
+ principalCV(account.address), // recipient
615
596
  ];
616
597
  // Post-conditions:
617
- // 1. pool reserve will send wSTX rewards to user
618
- // 2. sender pays small STX fee for Pyth oracle update (~2 uSTX)
598
+ // 1. Vault sends underlying to user (amount from convert-to-assets)
619
599
  const postConditions = [
620
- Pc.principal(this.contracts.poolReserve)
621
- .willSendGte(0n)
622
- .ft(this.contracts.wstx, wstxName),
623
- Pc.principal(account.address)
624
- .willSendLte(100n)
625
- .ustx(),
600
+ this.buildSendPC(assetConfig.vault, expectedUnderlying, assetConfig, "lte"),
626
601
  ];
627
602
  return callContract(account, {
628
603
  contractAddress: address,
629
604
  contractName: name,
630
- functionName: "claim-rewards",
605
+ functionName: "redeem",
631
606
  functionArgs,
632
607
  postConditionMode: PostConditionMode.Deny,
633
608
  postConditions,