@agether/sdk 1.6.0 → 1.6.2

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/index.js CHANGED
@@ -760,6 +760,179 @@ var MorphoClient = class {
760
760
  };
761
761
  }
762
762
  // ════════════════════════════════════════════════════════
763
+ // Balance & Borrowing Capacity
764
+ // ════════════════════════════════════════════════════════
765
+ /**
766
+ * Get the USDC balance of the AgentAccount.
767
+ * @returns USDC balance in raw units (6 decimals)
768
+ */
769
+ async getUsdcBalance() {
770
+ const acctAddr = await this.getAccountAddress();
771
+ const usdc = new import_ethers2.Contract(this.config.contracts.usdc, ERC20_ABI, this.provider);
772
+ return usdc.balanceOf(acctAddr);
773
+ }
774
+ /**
775
+ * Calculate the maximum additional USDC that can be borrowed
776
+ * given the agent's current collateral and debt across all markets.
777
+ *
778
+ * For each market with collateral deposited:
779
+ * maxBorrow = (collateralValue * LLTV) - currentDebt
780
+ *
781
+ * Uses the Morpho oracle to price collateral → loan token.
782
+ *
783
+ * @returns Maximum additional USDC borrowable (6 decimals)
784
+ */
785
+ async getMaxBorrowable() {
786
+ const acctAddr = await this.getAccountAddress();
787
+ const markets = await this.getMarkets();
788
+ let totalAdditional = 0n;
789
+ const byMarket = [];
790
+ for (const m of markets) {
791
+ if (!m.collateralAsset || m.collateralAsset.address === import_ethers2.ethers.ZeroAddress) continue;
792
+ try {
793
+ const pos = await this.morphoBlue.position(m.uniqueKey, acctAddr);
794
+ if (pos.collateral === 0n) continue;
795
+ const mktState = await this.morphoBlue.market(m.uniqueKey);
796
+ const totalBorrowShares = BigInt(mktState.totalBorrowShares);
797
+ const totalBorrowAssets = BigInt(mktState.totalBorrowAssets);
798
+ const currentDebt = totalBorrowShares > 0n ? BigInt(pos.borrowShares) * totalBorrowAssets / totalBorrowShares : 0n;
799
+ let collateralValueInLoan;
800
+ try {
801
+ const oracleContract = new import_ethers2.Contract(m.oracle, [
802
+ "function price() view returns (uint256)"
803
+ ], this.provider);
804
+ const oraclePrice = await oracleContract.price();
805
+ const ORACLE_PRICE_SCALE = 10n ** 36n;
806
+ collateralValueInLoan = BigInt(pos.collateral) * oraclePrice / ORACLE_PRICE_SCALE;
807
+ } catch {
808
+ continue;
809
+ }
810
+ const maxBorrowTotal = collateralValueInLoan * m.lltv / 10n ** 18n;
811
+ const maxAdditional = maxBorrowTotal > currentDebt ? maxBorrowTotal - currentDebt : 0n;
812
+ totalAdditional += maxAdditional;
813
+ byMarket.push({
814
+ collateralToken: m.collateralAsset.symbol,
815
+ maxAdditional,
816
+ currentDebt,
817
+ collateralValue: collateralValueInLoan
818
+ });
819
+ } catch {
820
+ continue;
821
+ }
822
+ }
823
+ return { total: totalAdditional, byMarket };
824
+ }
825
+ // ════════════════════════════════════════════════════════
826
+ // Market Rates & Yield Estimation
827
+ // ════════════════════════════════════════════════════════
828
+ /**
829
+ * Fetch current supply/borrow APY for a collateral market from Morpho GraphQL API.
830
+ *
831
+ * Note: On Morpho Blue, collateral does NOT earn yield directly. Supply APY
832
+ * is what lenders earn; borrow APY is what borrowers pay.
833
+ */
834
+ async getMarketRates(collateralSymbolOrAddress) {
835
+ const chainId = this.config.chainId;
836
+ const usdcAddr = this.config.contracts.usdc.toLowerCase();
837
+ let collateralFilter = "";
838
+ if (collateralSymbolOrAddress) {
839
+ const colInfo = BASE_COLLATERALS[collateralSymbolOrAddress];
840
+ const colAddr = (colInfo?.address ?? collateralSymbolOrAddress).toLowerCase();
841
+ collateralFilter = `, collateralAssetAddress_in: ["${colAddr}"]`;
842
+ }
843
+ const query = `{
844
+ markets(
845
+ first: 50
846
+ orderBy: SupplyAssetsUsd
847
+ orderDirection: Desc
848
+ where: { chainId_in: [${chainId}], loanAssetAddress_in: ["${usdcAddr}"]${collateralFilter} }
849
+ ) {
850
+ items {
851
+ uniqueKey
852
+ lltv
853
+ loanAsset { address symbol decimals }
854
+ collateralAsset { address symbol decimals }
855
+ state {
856
+ borrowAssets
857
+ supplyAssets
858
+ utilization
859
+ supplyApy
860
+ borrowApy
861
+ }
862
+ }
863
+ }
864
+ }`;
865
+ try {
866
+ const resp = await import_axios.default.post(MORPHO_API_URL, { query }, { timeout: 1e4 });
867
+ const items = resp.data?.data?.markets?.items ?? [];
868
+ return items.filter((m) => m.collateralAsset?.address && m.collateralAsset.address !== import_ethers2.ethers.ZeroAddress).map((m) => ({
869
+ collateralToken: m.collateralAsset.symbol,
870
+ loanToken: m.loanAsset.symbol,
871
+ supplyApy: m.state?.supplyApy ? Number(m.state.supplyApy) : 0,
872
+ borrowApy: m.state?.borrowApy ? Number(m.state.borrowApy) : 0,
873
+ utilization: m.state?.utilization ? Number(m.state.utilization) : 0,
874
+ totalSupplyUsd: m.state?.supplyAssets ? Number(m.state.supplyAssets) / 1e6 : 0,
875
+ totalBorrowUsd: m.state?.borrowAssets ? Number(m.state.borrowAssets) / 1e6 : 0,
876
+ lltv: `${(Number(m.lltv) / 1e16).toFixed(0)}%`,
877
+ marketId: m.uniqueKey
878
+ }));
879
+ } catch {
880
+ return [];
881
+ }
882
+ }
883
+ /**
884
+ * Estimate theoretical yield for a given collateral amount over a period.
885
+ *
886
+ * ⚠️ IMPORTANT: On Morpho Blue, collateral does NOT earn yield directly.
887
+ * This estimates what the collateral WOULD earn if it were instead supplied
888
+ * as a lender (not used as collateral). This is a theoretical upper bound
889
+ * useful for setting spending caps.
890
+ *
891
+ * @param collateralSymbol - e.g. 'WETH'
892
+ * @param amount - collateral amount in human-readable (e.g. '1.5')
893
+ * @param periodDays - estimation period in days (default: 1)
894
+ * @param ethPriceUsd - ETH price in USD for value conversion (if not provided, uses oracle)
895
+ * @returns Estimated yield in USD for the period
896
+ */
897
+ async getYieldEstimate(collateralSymbol, amount, periodDays = 1, ethPriceUsd) {
898
+ const colInfo = BASE_COLLATERALS[collateralSymbol];
899
+ if (!colInfo) throw new AgetherError(`Unknown collateral: ${collateralSymbol}`, "UNKNOWN_COLLATERAL");
900
+ const rates = await this.getMarketRates(collateralSymbol);
901
+ if (rates.length === 0) {
902
+ throw new AgetherError(`No market found for ${collateralSymbol}`, "MARKET_NOT_FOUND");
903
+ }
904
+ const market = rates[0];
905
+ const supplyApy = market.supplyApy;
906
+ let collateralValueUsd;
907
+ if (ethPriceUsd) {
908
+ collateralValueUsd = parseFloat(amount) * ethPriceUsd;
909
+ } else {
910
+ try {
911
+ const params = await this.findMarketForCollateral(collateralSymbol);
912
+ const oracleContract = new import_ethers2.Contract(params.oracle, [
913
+ "function price() view returns (uint256)"
914
+ ], this.provider);
915
+ const oraclePrice = await oracleContract.price();
916
+ const ORACLE_PRICE_SCALE = 10n ** 36n;
917
+ const amountWei = import_ethers2.ethers.parseUnits(amount, colInfo.decimals);
918
+ const valueInUsdc = amountWei * oraclePrice / ORACLE_PRICE_SCALE;
919
+ collateralValueUsd = Number(valueInUsdc) / 1e6;
920
+ } catch {
921
+ throw new AgetherError("Cannot determine collateral value. Provide ethPriceUsd.", "PRICE_UNAVAILABLE");
922
+ }
923
+ }
924
+ const estimatedYieldUsd = collateralValueUsd * supplyApy * (periodDays / 365);
925
+ return {
926
+ collateralToken: collateralSymbol,
927
+ amount,
928
+ periodDays,
929
+ theoreticalSupplyApy: supplyApy,
930
+ estimatedYieldUsd,
931
+ collateralValueUsd,
932
+ disclaimer: "Collateral on Morpho Blue does NOT earn yield directly. This estimates what it WOULD earn if supplied as a lender instead. Use as a theoretical spending cap."
933
+ };
934
+ }
935
+ // ════════════════════════════════════════════════════════
763
936
  // Lending Operations (all via AgentAccount.executeBatch)
764
937
  // ════════════════════════════════════════════════════════
765
938
  /**
@@ -891,7 +1064,6 @@ var MorphoClient = class {
891
1064
  */
892
1065
  async repay(usdcAmount, tokenSymbol, marketParams) {
893
1066
  const acctAddr = await this.getAccountAddress();
894
- const amount = import_ethers2.ethers.parseUnits(usdcAmount, 6);
895
1067
  const morphoAddr = this.config.contracts.morphoBlue;
896
1068
  const usdcAddr = this.config.contracts.usdc;
897
1069
  let params;
@@ -903,14 +1075,60 @@ var MorphoClient = class {
903
1075
  const { params: p } = await this._findActiveMarket();
904
1076
  params = p;
905
1077
  }
1078
+ let repayAssets;
1079
+ let repayShares;
1080
+ let approveAmount;
1081
+ if (usdcAmount === "all") {
1082
+ const markets = await this.getMarkets();
1083
+ const mkt = markets.find(
1084
+ (m) => m.collateralAsset?.address.toLowerCase() === params.collateralToken.toLowerCase()
1085
+ );
1086
+ if (mkt) {
1087
+ const pos = await this.morphoBlue.position(mkt.uniqueKey, acctAddr);
1088
+ repayShares = BigInt(pos.borrowShares);
1089
+ repayAssets = 0n;
1090
+ const onChainMkt = await this.morphoBlue.market(mkt.uniqueKey);
1091
+ const totalBorrowAssets = BigInt(onChainMkt.totalBorrowAssets);
1092
+ const totalBorrowShares = BigInt(onChainMkt.totalBorrowShares);
1093
+ const estimated = totalBorrowShares > 0n ? repayShares * totalBorrowAssets / totalBorrowShares + 10n : 0n;
1094
+ approveAmount = estimated > 0n ? estimated : import_ethers2.ethers.parseUnits("1", 6);
1095
+ } else {
1096
+ repayAssets = import_ethers2.ethers.parseUnits("999999", 6);
1097
+ repayShares = 0n;
1098
+ approveAmount = repayAssets;
1099
+ }
1100
+ } else {
1101
+ repayAssets = import_ethers2.ethers.parseUnits(usdcAmount, 6);
1102
+ repayShares = 0n;
1103
+ approveAmount = repayAssets;
1104
+ try {
1105
+ const markets = await this.getMarkets();
1106
+ const mkt = markets.find(
1107
+ (m) => m.collateralAsset?.address.toLowerCase() === params.collateralToken.toLowerCase()
1108
+ );
1109
+ if (mkt) {
1110
+ const pos = await this.morphoBlue.position(mkt.uniqueKey, acctAddr);
1111
+ const onChainMkt = await this.morphoBlue.market(mkt.uniqueKey);
1112
+ const totalBorrowAssets = BigInt(onChainMkt.totalBorrowAssets);
1113
+ const totalBorrowShares = BigInt(onChainMkt.totalBorrowShares);
1114
+ const currentDebt = totalBorrowShares > 0n ? BigInt(pos.borrowShares) * totalBorrowAssets / totalBorrowShares : 0n;
1115
+ if (repayAssets >= currentDebt && BigInt(pos.borrowShares) > 0n) {
1116
+ repayShares = BigInt(pos.borrowShares);
1117
+ repayAssets = 0n;
1118
+ approveAmount = currentDebt + 10n;
1119
+ }
1120
+ }
1121
+ } catch {
1122
+ }
1123
+ }
906
1124
  const targets = [usdcAddr, morphoAddr];
907
1125
  const values = [0n, 0n];
908
1126
  const datas = [
909
- erc20Iface.encodeFunctionData("approve", [morphoAddr, amount]),
1127
+ erc20Iface.encodeFunctionData("approve", [morphoAddr, approveAmount]),
910
1128
  morphoIface.encodeFunctionData("repay", [
911
1129
  this._toTuple(params),
912
- amount,
913
- 0n,
1130
+ repayAssets,
1131
+ repayShares,
914
1132
  acctAddr,
915
1133
  "0x"
916
1134
  ])
@@ -1107,6 +1325,9 @@ var X402Client = class {
1107
1325
  const client = new import_client.x402Client();
1108
1326
  (0, import_client2.registerExactEvmScheme)(client, { signer });
1109
1327
  this.paidFetch = (0, import_fetch.wrapFetchWithPayment)(fetch, client);
1328
+ const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
1329
+ const dailyLimit = config.dailySpendLimitUsdc ? BigInt(Math.round(parseFloat(config.dailySpendLimitUsdc) * 1e6)) : 0n;
1330
+ this._spendingTracker = { date: today, totalBorrowed: 0n, dailyLimit };
1110
1331
  }
1111
1332
  async get(url, opts) {
1112
1333
  return this.request(url, { ...opts, method: "GET" });
@@ -1122,6 +1343,82 @@ var X402Client = class {
1122
1343
  getAddress() {
1123
1344
  return this.address;
1124
1345
  }
1346
+ /** Get the current spending tracker state */
1347
+ getSpendingTracker() {
1348
+ this._resetTrackerIfNewDay();
1349
+ return { ...this._spendingTracker };
1350
+ }
1351
+ /** Get remaining daily spending allowance in USDC (human-readable) */
1352
+ getRemainingDailyAllowance() {
1353
+ this._resetTrackerIfNewDay();
1354
+ if (this._spendingTracker.dailyLimit === 0n) return "unlimited";
1355
+ const remaining = this._spendingTracker.dailyLimit - this._spendingTracker.totalBorrowed;
1356
+ return (Number(remaining > 0n ? remaining : 0n) / 1e6).toFixed(2);
1357
+ }
1358
+ /**
1359
+ * Pay with auto-draw: Make an x402 request with automatic Morpho borrowing.
1360
+ *
1361
+ * Flow:
1362
+ * 1. Check USDC balance on AgentAccount
1363
+ * 2. Probe the URL to discover payment amount (if 402)
1364
+ * 3. If insufficient USDC, calculate deficit
1365
+ * 4. Check spending limit
1366
+ * 5. Borrow from Morpho via MorphoClient
1367
+ * 6. Proceed with x402 payment
1368
+ */
1369
+ async payWithAutoDraw(url, opts) {
1370
+ const { morphoClient, ...fetchOpts } = opts || {};
1371
+ if (!this.config.autoDraw || !morphoClient) {
1372
+ return this.request(url, fetchOpts);
1373
+ }
1374
+ try {
1375
+ const usdcBalance = await morphoClient.getUsdcBalance();
1376
+ console.log(` [auto-draw] AgentAccount USDC balance: ${(Number(usdcBalance) / 1e6).toFixed(2)}`);
1377
+ const paymentAmount = await this._probePaymentAmount(url, fetchOpts);
1378
+ if (paymentAmount !== null) {
1379
+ console.log(` [auto-draw] Payment required: ${(Number(paymentAmount) / 1e6).toFixed(6)} USDC`);
1380
+ const bufferStr = this.config.autoDrawBuffer || "0.5";
1381
+ const buffer = BigInt(Math.round(parseFloat(bufferStr) * 1e6));
1382
+ const needed = paymentAmount + buffer;
1383
+ if (usdcBalance < needed) {
1384
+ const deficit = needed - usdcBalance;
1385
+ console.log(` [auto-draw] Insufficient balance. Need to borrow ${(Number(deficit) / 1e6).toFixed(2)} USDC`);
1386
+ const limitCheck = await this._checkSpendingLimit(deficit, morphoClient);
1387
+ if (!limitCheck.allowed) {
1388
+ return {
1389
+ success: false,
1390
+ error: `Auto-draw blocked: ${limitCheck.reason}`
1391
+ };
1392
+ }
1393
+ const maxBorrowable = await morphoClient.getMaxBorrowable();
1394
+ if (maxBorrowable.total < deficit) {
1395
+ return {
1396
+ success: false,
1397
+ error: `Auto-draw failed: insufficient collateral. Need ${(Number(deficit) / 1e6).toFixed(2)} USDC but can only borrow ${(Number(maxBorrowable.total) / 1e6).toFixed(2)} USDC more.`
1398
+ };
1399
+ }
1400
+ const borrowAmount = (Number(deficit) / 1e6).toFixed(6);
1401
+ console.log(` [auto-draw] Borrowing ${borrowAmount} USDC from Morpho...`);
1402
+ const borrowResult = await morphoClient.borrow(borrowAmount);
1403
+ console.log(` [auto-draw] Borrow tx: ${borrowResult.tx}`);
1404
+ this._trackSpending(deficit);
1405
+ const result = await this.request(url, fetchOpts);
1406
+ return {
1407
+ ...result,
1408
+ autoDrawInfo: {
1409
+ borrowed: borrowAmount,
1410
+ borrowTx: borrowResult.tx,
1411
+ reason: `USDC balance insufficient (had ${(Number(usdcBalance) / 1e6).toFixed(2)}, needed ${(Number(needed) / 1e6).toFixed(2)})`
1412
+ }
1413
+ };
1414
+ }
1415
+ }
1416
+ return this.request(url, fetchOpts);
1417
+ } catch (error) {
1418
+ console.log(` [auto-draw] Auto-draw check failed: ${error instanceof Error ? error.message : String(error)}. Proceeding with normal request.`);
1419
+ return this.request(url, fetchOpts);
1420
+ }
1421
+ }
1125
1422
  // ──────────── Core request — @x402/fetch handles 402 automatically ────────────
1126
1423
  async request(url, options) {
1127
1424
  try {
@@ -1171,6 +1468,102 @@ var X402Client = class {
1171
1468
  };
1172
1469
  }
1173
1470
  }
1471
+ // ──────────── Auto-Draw Helpers ────────────
1472
+ /**
1473
+ * Probe a URL to discover payment requirements without paying.
1474
+ * Makes a request and parses the 402 PAYMENT-REQUIRED header.
1475
+ * @returns Payment amount in raw USDC units (6 decimals), or null if not a 402.
1476
+ */
1477
+ async _probePaymentAmount(url, options) {
1478
+ try {
1479
+ const response = await fetch(url, {
1480
+ ...options,
1481
+ headers: {
1482
+ ...options?.headers,
1483
+ "X-Agent-Id": this.config.agentId || ""
1484
+ }
1485
+ });
1486
+ if (response.status !== 402) return null;
1487
+ const paymentHeader = response.headers.get("X-PAYMENT") || response.headers.get("PAYMENT-REQUIRED");
1488
+ if (!paymentHeader) return null;
1489
+ try {
1490
+ const decoded = JSON.parse(Buffer.from(paymentHeader, "base64").toString("utf-8"));
1491
+ const requirements = Array.isArray(decoded) ? decoded : decoded.accepts || [decoded];
1492
+ if (requirements.length > 0) {
1493
+ const amount = requirements[0].maxAmountRequired || requirements[0].amount;
1494
+ if (amount) {
1495
+ return BigInt(amount);
1496
+ }
1497
+ }
1498
+ } catch {
1499
+ }
1500
+ return null;
1501
+ } catch {
1502
+ return null;
1503
+ }
1504
+ }
1505
+ /** Reset spending tracker if it's a new day */
1506
+ _resetTrackerIfNewDay() {
1507
+ const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
1508
+ if (this._spendingTracker.date !== today) {
1509
+ this._spendingTracker = {
1510
+ date: today,
1511
+ totalBorrowed: 0n,
1512
+ dailyLimit: this._spendingTracker.dailyLimit
1513
+ };
1514
+ }
1515
+ }
1516
+ /** Track a new spending amount */
1517
+ _trackSpending(amount) {
1518
+ this._resetTrackerIfNewDay();
1519
+ this._spendingTracker.totalBorrowed += amount;
1520
+ }
1521
+ /**
1522
+ * Check if a borrow amount is within spending limits.
1523
+ * Considers both fixed daily limits and yield-limited spending.
1524
+ */
1525
+ async _checkSpendingLimit(amount, morphoClient) {
1526
+ this._resetTrackerIfNewDay();
1527
+ if (this.config.yieldLimitedSpending) {
1528
+ try {
1529
+ const status = await morphoClient.getStatus();
1530
+ let totalDailyYieldUsdc = 0;
1531
+ for (const pos of status.positions) {
1532
+ if (parseFloat(pos.collateral) > 0) {
1533
+ try {
1534
+ const estimate = await morphoClient.getYieldEstimate(
1535
+ pos.collateralToken,
1536
+ pos.collateral,
1537
+ 1
1538
+ // 1 day
1539
+ );
1540
+ totalDailyYieldUsdc += estimate.estimatedYieldUsd;
1541
+ } catch {
1542
+ }
1543
+ }
1544
+ }
1545
+ const yieldLimit = BigInt(Math.round(totalDailyYieldUsdc * 1e6));
1546
+ const newTotal = this._spendingTracker.totalBorrowed + amount;
1547
+ if (yieldLimit > 0n && newTotal > yieldLimit) {
1548
+ return {
1549
+ allowed: false,
1550
+ reason: `Yield-limited spending exceeded. Daily yield cap: $${(Number(yieldLimit) / 1e6).toFixed(2)}, already spent: $${(Number(this._spendingTracker.totalBorrowed) / 1e6).toFixed(2)}, requested: $${(Number(amount) / 1e6).toFixed(2)}`
1551
+ };
1552
+ }
1553
+ } catch {
1554
+ }
1555
+ }
1556
+ if (this._spendingTracker.dailyLimit > 0n) {
1557
+ const newTotal = this._spendingTracker.totalBorrowed + amount;
1558
+ if (newTotal > this._spendingTracker.dailyLimit) {
1559
+ return {
1560
+ allowed: false,
1561
+ reason: `Daily spending limit exceeded. Limit: $${(Number(this._spendingTracker.dailyLimit) / 1e6).toFixed(2)}, already spent: $${(Number(this._spendingTracker.totalBorrowed) / 1e6).toFixed(2)}, requested: $${(Number(amount) / 1e6).toFixed(2)}`
1562
+ };
1563
+ }
1564
+ }
1565
+ return { allowed: true };
1566
+ }
1174
1567
  };
1175
1568
 
1176
1569
  // src/clients/ScoringClient.ts