@t2000/sdk 0.4.0 → 0.4.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
@@ -1114,6 +1114,306 @@ var CetusAdapter = class {
1114
1114
  return getPoolPrice(this.client);
1115
1115
  }
1116
1116
  };
1117
+ var USDC_TYPE2 = SUPPORTED_ASSETS.USDC.type;
1118
+ SUPPORTED_ASSETS.USDC.decimals;
1119
+ var WAD = 1e18;
1120
+ var MIN_HEALTH_FACTOR2 = 1.5;
1121
+ function interpolateRate(utilBreakpoints, aprBreakpoints, utilizationPct) {
1122
+ if (utilBreakpoints.length === 0) return 0;
1123
+ if (utilizationPct <= utilBreakpoints[0]) return aprBreakpoints[0];
1124
+ if (utilizationPct >= utilBreakpoints[utilBreakpoints.length - 1]) {
1125
+ return aprBreakpoints[aprBreakpoints.length - 1];
1126
+ }
1127
+ for (let i = 1; i < utilBreakpoints.length; i++) {
1128
+ if (utilizationPct <= utilBreakpoints[i]) {
1129
+ const t = (utilizationPct - utilBreakpoints[i - 1]) / (utilBreakpoints[i] - utilBreakpoints[i - 1]);
1130
+ return aprBreakpoints[i - 1] + t * (aprBreakpoints[i] - aprBreakpoints[i - 1]);
1131
+ }
1132
+ }
1133
+ return aprBreakpoints[aprBreakpoints.length - 1];
1134
+ }
1135
+ function computeRatesFromReserve(reserve) {
1136
+ const decimals = reserve.mintDecimals;
1137
+ const available = Number(reserve.availableAmount) / 10 ** decimals;
1138
+ const borrowed = Number(reserve.borrowedAmount.value) / WAD / 10 ** decimals;
1139
+ const totalDeposited = available + borrowed;
1140
+ const utilizationPct = totalDeposited > 0 ? borrowed / totalDeposited * 100 : 0;
1141
+ const config = reserve.config.element;
1142
+ if (!config) return { borrowAprPct: 0, depositAprPct: 0, utilizationPct: 0 };
1143
+ const utils = config.interestRateUtils.map(Number);
1144
+ const aprs = config.interestRateAprs.map((a) => Number(a) / 100);
1145
+ const borrowAprPct = interpolateRate(utils, aprs, utilizationPct);
1146
+ const spreadFeeBps = Number(config.spreadFeeBps);
1147
+ const depositAprPct = utilizationPct / 100 * (borrowAprPct / 100) * (1 - spreadFeeBps / 1e4) * 100;
1148
+ return { borrowAprPct, depositAprPct, utilizationPct };
1149
+ }
1150
+ function cTokenRatio(reserve) {
1151
+ if (reserve.ctokenSupply === 0n) return 1;
1152
+ const available = Number(reserve.availableAmount);
1153
+ const borrowed = Number(reserve.borrowedAmount.value) / WAD;
1154
+ const spreadFees = Number(reserve.unclaimedSpreadFees.value) / WAD;
1155
+ const totalSupply = available + borrowed - spreadFees;
1156
+ return totalSupply / Number(reserve.ctokenSupply);
1157
+ }
1158
+ var SuilendAdapter = class {
1159
+ id = "suilend";
1160
+ name = "Suilend";
1161
+ version = "1.0.0";
1162
+ capabilities = ["save", "withdraw"];
1163
+ supportedAssets = ["USDC"];
1164
+ supportsSameAssetBorrow = false;
1165
+ client;
1166
+ suilend;
1167
+ lendingMarketType;
1168
+ initialized = false;
1169
+ initPromise = null;
1170
+ async init(client) {
1171
+ this.client = client;
1172
+ await this.lazyInit();
1173
+ }
1174
+ initSync(client) {
1175
+ this.client = client;
1176
+ }
1177
+ async lazyInit() {
1178
+ if (this.initialized) return;
1179
+ if (this.initPromise) return this.initPromise;
1180
+ this.initPromise = (async () => {
1181
+ let sdk;
1182
+ try {
1183
+ sdk = await import('@suilend/sdk');
1184
+ } catch {
1185
+ throw new T2000Error(
1186
+ "PROTOCOL_UNAVAILABLE",
1187
+ "Suilend SDK not installed. Run: npm install @suilend/sdk@^1"
1188
+ );
1189
+ }
1190
+ this.lendingMarketType = sdk.LENDING_MARKET_TYPE;
1191
+ try {
1192
+ this.suilend = await sdk.SuilendClient.initialize(
1193
+ sdk.LENDING_MARKET_ID,
1194
+ sdk.LENDING_MARKET_TYPE,
1195
+ this.client
1196
+ );
1197
+ } catch (err) {
1198
+ this.initPromise = null;
1199
+ throw new T2000Error(
1200
+ "PROTOCOL_UNAVAILABLE",
1201
+ `Failed to initialize Suilend: ${err instanceof Error ? err.message : String(err)}`
1202
+ );
1203
+ }
1204
+ this.initialized = true;
1205
+ })();
1206
+ return this.initPromise;
1207
+ }
1208
+ async ensureInit() {
1209
+ if (!this.initialized) {
1210
+ await this.lazyInit();
1211
+ }
1212
+ }
1213
+ findReserve(asset) {
1214
+ const upper = asset.toUpperCase();
1215
+ let coinType;
1216
+ if (upper === "USDC") coinType = USDC_TYPE2;
1217
+ else if (upper === "SUI") coinType = "0x2::sui::SUI";
1218
+ else if (asset.includes("::")) coinType = asset;
1219
+ else return void 0;
1220
+ try {
1221
+ const normalized = normalizeStructTag(coinType);
1222
+ return this.suilend.lendingMarket.reserves.find(
1223
+ (r) => normalizeStructTag(r.coinType.name) === normalized
1224
+ );
1225
+ } catch {
1226
+ return void 0;
1227
+ }
1228
+ }
1229
+ async getObligationCaps(address) {
1230
+ const SuilendClientStatic = (await import('@suilend/sdk')).SuilendClient;
1231
+ return SuilendClientStatic.getObligationOwnerCaps(
1232
+ address,
1233
+ [this.lendingMarketType],
1234
+ this.client
1235
+ );
1236
+ }
1237
+ resolveSymbol(coinType) {
1238
+ const normalized = normalizeStructTag(coinType);
1239
+ if (normalized === normalizeStructTag(USDC_TYPE2)) return "USDC";
1240
+ if (normalized === normalizeStructTag("0x2::sui::SUI")) return "SUI";
1241
+ const parts = coinType.split("::");
1242
+ return parts[parts.length - 1] || "UNKNOWN";
1243
+ }
1244
+ async getRates(asset) {
1245
+ await this.ensureInit();
1246
+ const reserve = this.findReserve(asset);
1247
+ if (!reserve) {
1248
+ throw new T2000Error("ASSET_NOT_SUPPORTED", `Suilend does not support ${asset}`);
1249
+ }
1250
+ const { borrowAprPct, depositAprPct } = computeRatesFromReserve(reserve);
1251
+ return {
1252
+ asset,
1253
+ saveApy: depositAprPct,
1254
+ borrowApy: borrowAprPct
1255
+ };
1256
+ }
1257
+ async getPositions(address) {
1258
+ await this.ensureInit();
1259
+ const supplies = [];
1260
+ const borrows = [];
1261
+ const caps = await this.getObligationCaps(address);
1262
+ if (caps.length === 0) return { supplies, borrows };
1263
+ const obligation = await this.suilend.getObligation(caps[0].obligationId);
1264
+ for (const deposit of obligation.deposits) {
1265
+ const coinType = normalizeStructTag(deposit.coinType.name);
1266
+ const reserve = this.suilend.lendingMarket.reserves.find(
1267
+ (r) => normalizeStructTag(r.coinType.name) === coinType
1268
+ );
1269
+ if (!reserve) continue;
1270
+ const ctokenAmount = Number(deposit.depositedCtokenAmount.toString());
1271
+ const ratio = cTokenRatio(reserve);
1272
+ const amount = ctokenAmount * ratio / 10 ** reserve.mintDecimals;
1273
+ const { depositAprPct } = computeRatesFromReserve(reserve);
1274
+ supplies.push({ asset: this.resolveSymbol(coinType), amount, apy: depositAprPct });
1275
+ }
1276
+ for (const borrow of obligation.borrows) {
1277
+ const coinType = normalizeStructTag(borrow.coinType.name);
1278
+ const reserve = this.suilend.lendingMarket.reserves.find(
1279
+ (r) => normalizeStructTag(r.coinType.name) === coinType
1280
+ );
1281
+ if (!reserve) continue;
1282
+ const rawBorrowed = Number(borrow.borrowedAmount.value.toString()) / WAD;
1283
+ const amount = rawBorrowed / 10 ** reserve.mintDecimals;
1284
+ const reserveRate = Number(reserve.cumulativeBorrowRate.value.toString()) / WAD;
1285
+ const posRate = Number(borrow.cumulativeBorrowRate.value.toString()) / WAD;
1286
+ const compounded = posRate > 0 ? amount * (reserveRate / posRate) : amount;
1287
+ const { borrowAprPct } = computeRatesFromReserve(reserve);
1288
+ borrows.push({ asset: this.resolveSymbol(coinType), amount: compounded, apy: borrowAprPct });
1289
+ }
1290
+ return { supplies, borrows };
1291
+ }
1292
+ async getHealth(address) {
1293
+ await this.ensureInit();
1294
+ const caps = await this.getObligationCaps(address);
1295
+ if (caps.length === 0) {
1296
+ return { healthFactor: Infinity, supplied: 0, borrowed: 0, maxBorrow: 0, liquidationThreshold: 0 };
1297
+ }
1298
+ const positions = await this.getPositions(address);
1299
+ const supplied = positions.supplies.reduce((s, p) => s + p.amount, 0);
1300
+ const borrowed = positions.borrows.reduce((s, p) => s + p.amount, 0);
1301
+ const reserve = this.findReserve("USDC");
1302
+ const closeLtv = reserve?.config?.element?.closeLtvPct ?? 75;
1303
+ const openLtv = reserve?.config?.element?.openLtvPct ?? 70;
1304
+ const liqThreshold = closeLtv / 100;
1305
+ const healthFactor = borrowed > 0 ? supplied * liqThreshold / borrowed : Infinity;
1306
+ const maxBorrow = Math.max(0, supplied * (openLtv / 100) - borrowed);
1307
+ return { healthFactor, supplied, borrowed, maxBorrow, liquidationThreshold: liqThreshold };
1308
+ }
1309
+ async buildSaveTx(address, amount, _asset, options) {
1310
+ await this.ensureInit();
1311
+ const rawAmount = usdcToRaw(amount).toString();
1312
+ const tx = new Transaction();
1313
+ tx.setSender(address);
1314
+ const caps = await this.getObligationCaps(address);
1315
+ let capRef;
1316
+ if (caps.length === 0) {
1317
+ const [newCap] = this.suilend.createObligation(tx);
1318
+ capRef = newCap;
1319
+ } else {
1320
+ capRef = caps[0].id;
1321
+ }
1322
+ const allCoins = await this.fetchAllCoins(address, USDC_TYPE2);
1323
+ if (allCoins.length === 0) {
1324
+ throw new T2000Error("INSUFFICIENT_BALANCE", "No USDC coins found");
1325
+ }
1326
+ const primaryCoinId = allCoins[0].coinObjectId;
1327
+ if (allCoins.length > 1) {
1328
+ tx.mergeCoins(
1329
+ tx.object(primaryCoinId),
1330
+ allCoins.slice(1).map((c) => tx.object(c.coinObjectId))
1331
+ );
1332
+ }
1333
+ const [depositCoin] = tx.splitCoins(tx.object(primaryCoinId), [rawAmount]);
1334
+ if (options?.collectFee) {
1335
+ addCollectFeeToTx(tx, depositCoin, "save");
1336
+ }
1337
+ this.suilend.deposit(depositCoin, USDC_TYPE2, capRef, tx);
1338
+ return { tx };
1339
+ }
1340
+ async buildWithdrawTx(address, amount, _asset) {
1341
+ await this.ensureInit();
1342
+ const caps = await this.getObligationCaps(address);
1343
+ if (caps.length === 0) {
1344
+ throw new T2000Error("NO_COLLATERAL", "No Suilend position found");
1345
+ }
1346
+ const positions = await this.getPositions(address);
1347
+ const usdcSupply = positions.supplies.find((s) => s.asset === "USDC");
1348
+ const deposited = usdcSupply?.amount ?? 0;
1349
+ const effectiveAmount = Math.min(amount, deposited);
1350
+ if (effectiveAmount <= 0) {
1351
+ throw new T2000Error("NO_COLLATERAL", "Nothing to withdraw from Suilend");
1352
+ }
1353
+ const rawAmount = usdcToRaw(effectiveAmount).toString();
1354
+ const tx = new Transaction();
1355
+ tx.setSender(address);
1356
+ await this.suilend.withdrawAndSendToUser(
1357
+ address,
1358
+ caps[0].id,
1359
+ caps[0].obligationId,
1360
+ USDC_TYPE2,
1361
+ rawAmount,
1362
+ tx
1363
+ );
1364
+ return { tx, effectiveAmount };
1365
+ }
1366
+ async buildBorrowTx(_address, _amount, _asset, _options) {
1367
+ throw new T2000Error(
1368
+ "ASSET_NOT_SUPPORTED",
1369
+ "SuilendAdapter.buildBorrowTx() not available \u2014 Suilend requires different collateral/borrow assets. Deferred to Phase 10."
1370
+ );
1371
+ }
1372
+ async buildRepayTx(_address, _amount, _asset) {
1373
+ throw new T2000Error(
1374
+ "ASSET_NOT_SUPPORTED",
1375
+ "SuilendAdapter.buildRepayTx() not available \u2014 deferred to Phase 10."
1376
+ );
1377
+ }
1378
+ async maxWithdraw(address, _asset) {
1379
+ await this.ensureInit();
1380
+ const health = await this.getHealth(address);
1381
+ let maxAmount;
1382
+ if (health.borrowed === 0) {
1383
+ maxAmount = health.supplied;
1384
+ } else {
1385
+ maxAmount = Math.max(
1386
+ 0,
1387
+ health.supplied - health.borrowed * MIN_HEALTH_FACTOR2 / health.liquidationThreshold
1388
+ );
1389
+ }
1390
+ const remainingSupply = health.supplied - maxAmount;
1391
+ const hfAfter = health.borrowed > 0 ? remainingSupply * health.liquidationThreshold / health.borrowed : Infinity;
1392
+ return {
1393
+ maxAmount,
1394
+ healthFactorAfter: hfAfter,
1395
+ currentHF: health.healthFactor
1396
+ };
1397
+ }
1398
+ async maxBorrow(_address, _asset) {
1399
+ throw new T2000Error(
1400
+ "ASSET_NOT_SUPPORTED",
1401
+ "SuilendAdapter.maxBorrow() not available \u2014 deferred to Phase 10."
1402
+ );
1403
+ }
1404
+ async fetchAllCoins(owner, coinType) {
1405
+ const all = [];
1406
+ let cursor = null;
1407
+ let hasNext = true;
1408
+ while (hasNext) {
1409
+ const page = await this.client.getCoins({ owner, coinType, cursor: cursor ?? void 0 });
1410
+ all.push(...page.data.map((c) => ({ coinObjectId: c.coinObjectId, balance: c.balance })));
1411
+ cursor = page.nextCursor;
1412
+ hasNext = page.hasNextPage;
1413
+ }
1414
+ return all;
1415
+ }
1416
+ };
1117
1417
  function hasLeadingZeroBits(hash, bits) {
1118
1418
  const fullBytes = Math.floor(bits / 8);
1119
1419
  const remainingBits = bits % 8;
@@ -1362,6 +1662,9 @@ var T2000 = class _T2000 extends EventEmitter {
1362
1662
  const cetusAdapter = new CetusAdapter();
1363
1663
  cetusAdapter.initSync(client);
1364
1664
  registry.registerSwap(cetusAdapter);
1665
+ const suilendAdapter = new SuilendAdapter();
1666
+ suilendAdapter.initSync(client);
1667
+ registry.registerLending(suilendAdapter);
1365
1668
  return registry;
1366
1669
  }
1367
1670
  static async create(options = {}) {
@@ -1536,667 +1839,378 @@ var T2000 = class _T2000 extends EventEmitter {
1536
1839
  };
1537
1840
  }
1538
1841
  async withdraw(params) {
1539
- const asset = (params.asset ?? "USDC").toUpperCase();
1540
- if (asset !== "USDC") {
1541
- throw new T2000Error("ASSET_NOT_SUPPORTED", `Only USDC is supported for withdraw. Got: ${asset}`);
1542
- }
1543
- const adapter = await this.resolveLending(params.protocol, asset, "withdraw");
1544
- let amount;
1545
- if (params.amount === "all") {
1546
- const maxResult = await adapter.maxWithdraw(this._address, asset);
1547
- amount = maxResult.maxAmount;
1548
- if (amount <= 0) {
1549
- throw new T2000Error("NO_COLLATERAL", "No savings to withdraw");
1550
- }
1551
- } else {
1552
- amount = params.amount;
1553
- const hf = await adapter.getHealth(this._address);
1554
- if (hf.borrowed > 0) {
1555
- const maxResult = await adapter.maxWithdraw(this._address, asset);
1556
- if (amount > maxResult.maxAmount) {
1557
- throw new T2000Error(
1558
- "WITHDRAW_WOULD_LIQUIDATE",
1559
- `Withdrawing $${amount.toFixed(2)} would drop health factor below 1.5`,
1560
- {
1561
- safeWithdrawAmount: maxResult.maxAmount,
1562
- currentHF: maxResult.currentHF,
1563
- projectedHF: maxResult.healthFactorAfter
1564
- }
1565
- );
1566
- }
1567
- }
1568
- }
1569
- const withdrawAmount = amount;
1570
- let effectiveAmount = withdrawAmount;
1571
- const gasResult = await executeWithGas(this.client, this.keypair, async () => {
1572
- const built = await adapter.buildWithdrawTx(this._address, withdrawAmount, asset);
1573
- effectiveAmount = built.effectiveAmount;
1574
- return built.tx;
1575
- });
1576
- this.emitBalanceChange("USDC", effectiveAmount, "withdraw", gasResult.digest);
1577
- return {
1578
- success: true,
1579
- tx: gasResult.digest,
1580
- amount: effectiveAmount,
1581
- gasCost: gasResult.gasCostSui,
1582
- gasMethod: gasResult.gasMethod
1583
- };
1584
- }
1585
- async maxWithdraw() {
1586
- const adapter = await this.resolveLending(void 0, "USDC", "withdraw");
1587
- return adapter.maxWithdraw(this._address, "USDC");
1588
- }
1589
- // -- Borrowing --
1590
- async borrow(params) {
1591
- const asset = (params.asset ?? "USDC").toUpperCase();
1592
- if (asset !== "USDC") {
1593
- throw new T2000Error("ASSET_NOT_SUPPORTED", `Only USDC is supported for borrow. Got: ${asset}`);
1594
- }
1595
- const adapter = await this.resolveLending(params.protocol, asset, "borrow");
1596
- const maxResult = await adapter.maxBorrow(this._address, asset);
1597
- if (params.amount > maxResult.maxAmount) {
1598
- throw new T2000Error("HEALTH_FACTOR_TOO_LOW", `Max safe borrow: $${maxResult.maxAmount.toFixed(2)}`, {
1599
- maxBorrow: maxResult.maxAmount,
1600
- currentHF: maxResult.currentHF
1601
- });
1602
- }
1603
- const fee = calculateFee("borrow", params.amount);
1604
- const borrowAmount = params.amount;
1605
- const gasResult = await executeWithGas(this.client, this.keypair, async () => {
1606
- const { tx } = await adapter.buildBorrowTx(this._address, borrowAmount, asset, { collectFee: true });
1607
- return tx;
1608
- });
1609
- const hf = await adapter.getHealth(this._address);
1610
- reportFee(this._address, "borrow", fee.amount, fee.rate, gasResult.digest);
1611
- this.emitBalanceChange("USDC", borrowAmount, "borrow", gasResult.digest);
1612
- return {
1613
- success: true,
1614
- tx: gasResult.digest,
1615
- amount: borrowAmount,
1616
- fee: fee.amount,
1617
- healthFactor: hf.healthFactor,
1618
- gasCost: gasResult.gasCostSui,
1619
- gasMethod: gasResult.gasMethod
1620
- };
1621
- }
1622
- async repay(params) {
1623
- const asset = (params.asset ?? "USDC").toUpperCase();
1624
- if (asset !== "USDC") {
1625
- throw new T2000Error("ASSET_NOT_SUPPORTED", `Only USDC is supported for repay. Got: ${asset}`);
1626
- }
1627
- const adapter = await this.resolveLending(params.protocol, asset, "repay");
1628
- let amount;
1629
- if (params.amount === "all") {
1630
- const hf2 = await adapter.getHealth(this._address);
1631
- amount = hf2.borrowed;
1632
- if (amount <= 0) {
1633
- throw new T2000Error("NO_COLLATERAL", "No outstanding borrow to repay");
1634
- }
1635
- } else {
1636
- amount = params.amount;
1637
- }
1638
- const repayAmount = amount;
1639
- const gasResult = await executeWithGas(this.client, this.keypair, async () => {
1640
- const { tx } = await adapter.buildRepayTx(this._address, repayAmount, asset);
1641
- return tx;
1642
- });
1643
- const hf = await adapter.getHealth(this._address);
1644
- this.emitBalanceChange("USDC", repayAmount, "repay", gasResult.digest);
1645
- return {
1646
- success: true,
1647
- tx: gasResult.digest,
1648
- amount: repayAmount,
1649
- remainingDebt: hf.borrowed,
1650
- gasCost: gasResult.gasCostSui,
1651
- gasMethod: gasResult.gasMethod
1652
- };
1653
- }
1654
- async maxBorrow() {
1655
- const adapter = await this.resolveLending(void 0, "USDC", "borrow");
1656
- return adapter.maxBorrow(this._address, "USDC");
1657
- }
1658
- async healthFactor() {
1659
- const adapter = await this.resolveLending(void 0, "USDC", "save");
1660
- const hf = await adapter.getHealth(this._address);
1661
- if (hf.healthFactor < 1.2) {
1662
- this.emit("healthCritical", { healthFactor: hf.healthFactor, threshold: 1.5, severity: "critical" });
1663
- } else if (hf.healthFactor < 2) {
1664
- this.emit("healthWarning", { healthFactor: hf.healthFactor, threshold: 2, severity: "warning" });
1665
- }
1666
- return hf;
1667
- }
1668
- // -- Swap --
1669
- async swap(params) {
1670
- const fromAsset = params.from.toUpperCase();
1671
- const toAsset = params.to.toUpperCase();
1672
- if (!(fromAsset in SUPPORTED_ASSETS) || !(toAsset in SUPPORTED_ASSETS)) {
1673
- throw new T2000Error("ASSET_NOT_SUPPORTED", `Swap pair ${fromAsset}/${toAsset} is not supported`);
1674
- }
1675
- if (fromAsset === toAsset) {
1676
- throw new T2000Error("INVALID_AMOUNT", "Cannot swap same asset");
1677
- }
1678
- let adapter;
1679
- if (params.protocol) {
1680
- const found = this.registry.getSwap(params.protocol);
1681
- if (!found) throw new T2000Error("ASSET_NOT_SUPPORTED", `Swap adapter '${params.protocol}' not found`);
1682
- adapter = found;
1842
+ const asset = (params.asset ?? "USDC").toUpperCase();
1843
+ if (asset !== "USDC") {
1844
+ throw new T2000Error("ASSET_NOT_SUPPORTED", `Only USDC is supported for withdraw. Got: ${asset}`);
1845
+ }
1846
+ const adapter = await this.resolveLending(params.protocol, asset, "withdraw");
1847
+ let amount;
1848
+ if (params.amount === "all") {
1849
+ const maxResult = await adapter.maxWithdraw(this._address, asset);
1850
+ amount = maxResult.maxAmount;
1851
+ if (amount <= 0) {
1852
+ throw new T2000Error("NO_COLLATERAL", "No savings to withdraw");
1853
+ }
1683
1854
  } else {
1684
- const best = await this.registry.bestSwapQuote(fromAsset, toAsset, params.amount);
1685
- adapter = best.adapter;
1855
+ amount = params.amount;
1856
+ const hf = await adapter.getHealth(this._address);
1857
+ if (hf.borrowed > 0) {
1858
+ const maxResult = await adapter.maxWithdraw(this._address, asset);
1859
+ if (amount > maxResult.maxAmount) {
1860
+ throw new T2000Error(
1861
+ "WITHDRAW_WOULD_LIQUIDATE",
1862
+ `Withdrawing $${amount.toFixed(2)} would drop health factor below 1.5`,
1863
+ {
1864
+ safeWithdrawAmount: maxResult.maxAmount,
1865
+ currentHF: maxResult.currentHF,
1866
+ projectedHF: maxResult.healthFactorAfter
1867
+ }
1868
+ );
1869
+ }
1870
+ }
1686
1871
  }
1687
- const fee = calculateFee("swap", params.amount);
1688
- const swapAmount = params.amount;
1689
- const slippageBps = params.maxSlippage ? params.maxSlippage * 100 : void 0;
1690
- let swapMeta = { estimatedOut: 0, toDecimals: 0 };
1872
+ const withdrawAmount = amount;
1873
+ let effectiveAmount = withdrawAmount;
1691
1874
  const gasResult = await executeWithGas(this.client, this.keypair, async () => {
1692
- const built = await adapter.buildSwapTx(this._address, fromAsset, toAsset, swapAmount, slippageBps);
1693
- swapMeta = { estimatedOut: built.estimatedOut, toDecimals: built.toDecimals };
1875
+ const built = await adapter.buildWithdrawTx(this._address, withdrawAmount, asset);
1876
+ effectiveAmount = built.effectiveAmount;
1694
1877
  return built.tx;
1695
1878
  });
1696
- const toInfo = SUPPORTED_ASSETS[toAsset];
1697
- const txDetail = await this.client.getTransactionBlock({
1698
- digest: gasResult.digest,
1699
- options: { showBalanceChanges: true }
1700
- });
1701
- let actualReceived = 0;
1702
- if (txDetail.balanceChanges) {
1703
- for (const change of txDetail.balanceChanges) {
1704
- if (change.coinType === toInfo.type && change.owner && typeof change.owner === "object" && "AddressOwner" in change.owner && change.owner.AddressOwner === this._address) {
1705
- const amt = Number(change.amount) / 10 ** toInfo.decimals;
1706
- if (amt > 0) actualReceived += amt;
1707
- }
1708
- }
1709
- }
1710
- const expectedOutput = swapMeta.estimatedOut / 10 ** swapMeta.toDecimals;
1711
- if (actualReceived === 0) actualReceived = expectedOutput;
1712
- const priceImpact = expectedOutput > 0 ? Math.abs(actualReceived - expectedOutput) / expectedOutput : 0;
1713
- reportFee(this._address, "swap", fee.amount, fee.rate, gasResult.digest);
1714
- this.emitBalanceChange(fromAsset, swapAmount, "swap", gasResult.digest);
1879
+ this.emitBalanceChange("USDC", effectiveAmount, "withdraw", gasResult.digest);
1715
1880
  return {
1716
1881
  success: true,
1717
1882
  tx: gasResult.digest,
1718
- fromAmount: swapAmount,
1719
- fromAsset,
1720
- toAmount: actualReceived,
1721
- toAsset,
1722
- priceImpact,
1723
- fee: fee.amount,
1883
+ amount: effectiveAmount,
1724
1884
  gasCost: gasResult.gasCostSui,
1725
1885
  gasMethod: gasResult.gasMethod
1726
1886
  };
1727
1887
  }
1728
- async swapQuote(params) {
1729
- const fromAsset = params.from.toUpperCase();
1730
- const toAsset = params.to.toUpperCase();
1731
- const best = await this.registry.bestSwapQuote(fromAsset, toAsset, params.amount);
1732
- const fee = calculateFee("swap", params.amount);
1733
- return { ...best.quote, fee: { amount: fee.amount, rate: fee.rate } };
1734
- }
1735
- // -- Info --
1736
- async positions() {
1737
- const allPositions = await this.registry.allPositions(this._address);
1738
- const positions = allPositions.flatMap(
1739
- (p) => [
1740
- ...p.positions.supplies.map((s) => ({
1741
- protocol: p.protocolId,
1742
- asset: s.asset,
1743
- type: "save",
1744
- amount: s.amount,
1745
- apy: s.apy
1746
- })),
1747
- ...p.positions.borrows.map((b) => ({
1748
- protocol: p.protocolId,
1749
- asset: b.asset,
1750
- type: "borrow",
1751
- amount: b.amount,
1752
- apy: b.apy
1753
- }))
1754
- ]
1755
- );
1756
- return { positions };
1757
- }
1758
- async rates() {
1759
- return getRates(this.client);
1760
- }
1761
- async allRates(asset = "USDC") {
1762
- return this.registry.allRates(asset);
1763
- }
1764
- async earnings() {
1765
- const result = await getEarnings(this.client, this.keypair);
1766
- if (result.totalYieldEarned > 0) {
1767
- this.emit("yield", {
1768
- earned: result.dailyEarning,
1769
- total: result.totalYieldEarned,
1770
- apy: result.currentApy / 100,
1771
- timestamp: Date.now()
1772
- });
1773
- }
1774
- return result;
1775
- }
1776
- async fundStatus() {
1777
- return getFundStatus(this.client, this.keypair);
1778
- }
1779
- // -- Sentinel --
1780
- async sentinelList() {
1781
- return listSentinels();
1782
- }
1783
- async sentinelInfo(id) {
1784
- return getSentinelInfo(this.client, id);
1785
- }
1786
- async sentinelAttack(id, prompt, fee) {
1787
- return attack(this.client, this.keypair, id, prompt, fee);
1888
+ async maxWithdraw() {
1889
+ const adapter = await this.resolveLending(void 0, "USDC", "withdraw");
1890
+ return adapter.maxWithdraw(this._address, "USDC");
1788
1891
  }
1789
- // -- Helpers --
1790
- async resolveLending(protocol, asset, capability) {
1791
- if (protocol) {
1792
- const adapter = this.registry.getLending(protocol);
1793
- if (!adapter) throw new T2000Error("ASSET_NOT_SUPPORTED", `Lending adapter '${protocol}' not found`);
1794
- return adapter;
1795
- }
1796
- if (capability === "save") {
1797
- const { adapter } = await this.registry.bestSaveRate(asset);
1798
- return adapter;
1799
- }
1800
- if (capability === "borrow" || capability === "repay") {
1801
- const adapters2 = this.registry.listLending().filter(
1802
- (a) => a.supportedAssets.includes(asset) && a.capabilities.includes(capability) && (capability !== "borrow" || a.supportsSameAssetBorrow)
1803
- );
1804
- if (adapters2.length === 0) throw new T2000Error("ASSET_NOT_SUPPORTED", `No adapter supports ${capability} ${asset}`);
1805
- return adapters2[0];
1892
+ // -- Borrowing --
1893
+ async borrow(params) {
1894
+ const asset = (params.asset ?? "USDC").toUpperCase();
1895
+ if (asset !== "USDC") {
1896
+ throw new T2000Error("ASSET_NOT_SUPPORTED", `Only USDC is supported for borrow. Got: ${asset}`);
1806
1897
  }
1807
- const adapters = this.registry.listLending().filter(
1808
- (a) => a.supportedAssets.includes(asset) && a.capabilities.includes(capability)
1809
- );
1810
- if (adapters.length === 0) throw new T2000Error("ASSET_NOT_SUPPORTED", `No adapter supports ${capability} ${asset}`);
1811
- return adapters[0];
1812
- }
1813
- emitBalanceChange(asset, amount, cause, tx) {
1814
- this.emit("balanceChange", { asset, previous: 0, current: 0, cause, tx });
1815
- }
1816
- };
1817
- async function callSponsorApi(address, name) {
1818
- const res = await fetch(`${API_BASE_URL}/api/sponsor`, {
1819
- method: "POST",
1820
- headers: { "Content-Type": "application/json" },
1821
- body: JSON.stringify({ address, name })
1822
- });
1823
- if (res.status === 429) {
1824
- const data = await res.json();
1825
- if (data.challenge) {
1826
- const proof = solveHashcash(data.challenge);
1827
- const retry = await fetch(`${API_BASE_URL}/api/sponsor`, {
1828
- method: "POST",
1829
- headers: { "Content-Type": "application/json" },
1830
- body: JSON.stringify({ address, name, proof })
1898
+ const adapter = await this.resolveLending(params.protocol, asset, "borrow");
1899
+ const maxResult = await adapter.maxBorrow(this._address, asset);
1900
+ if (params.amount > maxResult.maxAmount) {
1901
+ throw new T2000Error("HEALTH_FACTOR_TOO_LOW", `Max safe borrow: $${maxResult.maxAmount.toFixed(2)}`, {
1902
+ maxBorrow: maxResult.maxAmount,
1903
+ currentHF: maxResult.currentHF
1831
1904
  });
1832
- if (!retry.ok) throw new T2000Error("SPONSOR_RATE_LIMITED", "Sponsor rate limited");
1833
- return;
1834
1905
  }
1835
- }
1836
- if (!res.ok) {
1837
- throw new T2000Error("SPONSOR_FAILED", "Sponsor API unavailable");
1838
- }
1839
- }
1840
-
1841
- // src/utils/simulate.ts
1842
- async function simulateTransaction(client, tx, sender) {
1843
- tx.setSender(sender);
1844
- try {
1845
- const txBytes = await tx.build({ client });
1846
- const dryRun = await client.dryRunTransactionBlock({
1847
- transactionBlock: Buffer.from(txBytes).toString("base64")
1906
+ const fee = calculateFee("borrow", params.amount);
1907
+ const borrowAmount = params.amount;
1908
+ const gasResult = await executeWithGas(this.client, this.keypair, async () => {
1909
+ const { tx } = await adapter.buildBorrowTx(this._address, borrowAmount, asset, { collectFee: true });
1910
+ return tx;
1848
1911
  });
1849
- const status = dryRun.effects?.status;
1850
- const gasUsed = dryRun.effects?.gasUsed;
1851
- const gasEstimateSui = gasUsed ? (Number(gasUsed.computationCost) + Number(gasUsed.storageCost) - Number(gasUsed.storageRebate)) / 1e9 : 0;
1852
- if (status?.status === "failure") {
1853
- const rawError = status.error ?? "Unknown simulation error";
1854
- const parsed = parseMoveAbort(rawError);
1855
- return {
1856
- success: false,
1857
- gasEstimateSui,
1858
- error: {
1859
- moveAbortCode: parsed.abortCode,
1860
- moveModule: parsed.module,
1861
- reason: parsed.reason,
1862
- rawError
1863
- }
1864
- };
1865
- }
1866
- return { success: true, gasEstimateSui };
1867
- } catch (err) {
1868
- const rawError = err instanceof Error ? err.message : String(err);
1912
+ const hf = await adapter.getHealth(this._address);
1913
+ reportFee(this._address, "borrow", fee.amount, fee.rate, gasResult.digest);
1914
+ this.emitBalanceChange("USDC", borrowAmount, "borrow", gasResult.digest);
1869
1915
  return {
1870
- success: false,
1871
- gasEstimateSui: 0,
1872
- error: {
1873
- reason: "Simulation failed: " + rawError,
1874
- rawError
1875
- }
1876
- };
1877
- }
1878
- }
1879
- function throwIfSimulationFailed(sim) {
1880
- if (sim.success) return;
1881
- throw new T2000Error(
1882
- "SIMULATION_FAILED",
1883
- sim.error?.reason ?? "Transaction simulation failed",
1884
- {
1885
- moveAbortCode: sim.error?.moveAbortCode,
1886
- moveModule: sim.error?.moveModule,
1887
- reason: sim.error?.reason,
1888
- rawError: sim.error?.rawError
1889
- }
1890
- );
1891
- }
1892
- function parseMoveAbort(errorStr) {
1893
- const abortMatch = errorStr.match(/MoveAbort\([^,]*,\s*(\d+)\)/);
1894
- const moduleMatch = errorStr.match(/name:\s*Identifier\("([^"]+)"\)/);
1895
- if (abortMatch) {
1896
- const code = parseInt(abortMatch[1], 10);
1897
- const module = moduleMatch?.[1];
1898
- const reason = mapMoveAbortCode(code);
1899
- return { abortCode: code, module, reason };
1916
+ success: true,
1917
+ tx: gasResult.digest,
1918
+ amount: borrowAmount,
1919
+ fee: fee.amount,
1920
+ healthFactor: hf.healthFactor,
1921
+ gasCost: gasResult.gasCostSui,
1922
+ gasMethod: gasResult.gasMethod
1923
+ };
1900
1924
  }
1901
- if (errorStr.includes("MovePrimitiveRuntimeError")) {
1902
- const module = moduleMatch?.[1];
1925
+ async repay(params) {
1926
+ const asset = (params.asset ?? "USDC").toUpperCase();
1927
+ if (asset !== "USDC") {
1928
+ throw new T2000Error("ASSET_NOT_SUPPORTED", `Only USDC is supported for repay. Got: ${asset}`);
1929
+ }
1930
+ const adapter = await this.resolveLending(params.protocol, asset, "repay");
1931
+ let amount;
1932
+ if (params.amount === "all") {
1933
+ const hf2 = await adapter.getHealth(this._address);
1934
+ amount = hf2.borrowed;
1935
+ if (amount <= 0) {
1936
+ throw new T2000Error("NO_COLLATERAL", "No outstanding borrow to repay");
1937
+ }
1938
+ } else {
1939
+ amount = params.amount;
1940
+ }
1941
+ const repayAmount = amount;
1942
+ const gasResult = await executeWithGas(this.client, this.keypair, async () => {
1943
+ const { tx } = await adapter.buildRepayTx(this._address, repayAmount, asset);
1944
+ return tx;
1945
+ });
1946
+ const hf = await adapter.getHealth(this._address);
1947
+ this.emitBalanceChange("USDC", repayAmount, "repay", gasResult.digest);
1903
1948
  return {
1904
- module,
1905
- reason: `Move runtime error in ${module ?? "unknown"} module`
1949
+ success: true,
1950
+ tx: gasResult.digest,
1951
+ amount: repayAmount,
1952
+ remainingDebt: hf.borrowed,
1953
+ gasCost: gasResult.gasCostSui,
1954
+ gasMethod: gasResult.gasMethod
1906
1955
  };
1907
1956
  }
1908
- return { reason: errorStr };
1909
- }
1910
- var USDC_TYPE2 = SUPPORTED_ASSETS.USDC.type;
1911
- SUPPORTED_ASSETS.USDC.decimals;
1912
- var WAD = 1e18;
1913
- var MIN_HEALTH_FACTOR2 = 1.5;
1914
- function interpolateRate(utilBreakpoints, aprBreakpoints, utilizationPct) {
1915
- if (utilBreakpoints.length === 0) return 0;
1916
- if (utilizationPct <= utilBreakpoints[0]) return aprBreakpoints[0];
1917
- if (utilizationPct >= utilBreakpoints[utilBreakpoints.length - 1]) {
1918
- return aprBreakpoints[aprBreakpoints.length - 1];
1957
+ async maxBorrow() {
1958
+ const adapter = await this.resolveLending(void 0, "USDC", "borrow");
1959
+ return adapter.maxBorrow(this._address, "USDC");
1919
1960
  }
1920
- for (let i = 1; i < utilBreakpoints.length; i++) {
1921
- if (utilizationPct <= utilBreakpoints[i]) {
1922
- const t = (utilizationPct - utilBreakpoints[i - 1]) / (utilBreakpoints[i] - utilBreakpoints[i - 1]);
1923
- return aprBreakpoints[i - 1] + t * (aprBreakpoints[i] - aprBreakpoints[i - 1]);
1961
+ async healthFactor() {
1962
+ const adapter = await this.resolveLending(void 0, "USDC", "save");
1963
+ const hf = await adapter.getHealth(this._address);
1964
+ if (hf.healthFactor < 1.2) {
1965
+ this.emit("healthCritical", { healthFactor: hf.healthFactor, threshold: 1.5, severity: "critical" });
1966
+ } else if (hf.healthFactor < 2) {
1967
+ this.emit("healthWarning", { healthFactor: hf.healthFactor, threshold: 2, severity: "warning" });
1924
1968
  }
1969
+ return hf;
1925
1970
  }
1926
- return aprBreakpoints[aprBreakpoints.length - 1];
1927
- }
1928
- function computeRatesFromReserve(reserve) {
1929
- const decimals = reserve.mintDecimals;
1930
- const available = Number(reserve.availableAmount) / 10 ** decimals;
1931
- const borrowed = Number(reserve.borrowedAmount.value) / WAD / 10 ** decimals;
1932
- const totalDeposited = available + borrowed;
1933
- const utilizationPct = totalDeposited > 0 ? borrowed / totalDeposited * 100 : 0;
1934
- const config = reserve.config.element;
1935
- if (!config) return { borrowAprPct: 0, depositAprPct: 0, utilizationPct: 0 };
1936
- const utils = config.interestRateUtils.map(Number);
1937
- const aprs = config.interestRateAprs.map((a) => Number(a) / 100);
1938
- const borrowAprPct = interpolateRate(utils, aprs, utilizationPct);
1939
- const spreadFeeBps = Number(config.spreadFeeBps);
1940
- const depositAprPct = utilizationPct / 100 * (borrowAprPct / 100) * (1 - spreadFeeBps / 1e4) * 100;
1941
- return { borrowAprPct, depositAprPct, utilizationPct };
1942
- }
1943
- function cTokenRatio(reserve) {
1944
- if (reserve.ctokenSupply === 0n) return 1;
1945
- const available = Number(reserve.availableAmount);
1946
- const borrowed = Number(reserve.borrowedAmount.value) / WAD;
1947
- const spreadFees = Number(reserve.unclaimedSpreadFees.value) / WAD;
1948
- const totalSupply = available + borrowed - spreadFees;
1949
- return totalSupply / Number(reserve.ctokenSupply);
1950
- }
1951
- var SuilendAdapter = class {
1952
- id = "suilend";
1953
- name = "Suilend";
1954
- version = "1.0.0";
1955
- capabilities = ["save", "withdraw"];
1956
- supportedAssets = ["USDC"];
1957
- supportsSameAssetBorrow = false;
1958
- client;
1959
- suilend;
1960
- lendingMarketType;
1961
- initialized = false;
1962
- async init(client) {
1963
- let sdk;
1964
- try {
1965
- sdk = await import('@suilend/sdk');
1966
- } catch {
1967
- throw new T2000Error(
1968
- "PROTOCOL_UNAVAILABLE",
1969
- "Suilend SDK not installed. Run: npm install @suilend/sdk@^1"
1970
- );
1971
- }
1972
- this.client = client;
1973
- this.lendingMarketType = sdk.LENDING_MARKET_TYPE;
1974
- try {
1975
- this.suilend = await sdk.SuilendClient.initialize(
1976
- sdk.LENDING_MARKET_ID,
1977
- sdk.LENDING_MARKET_TYPE,
1978
- client
1979
- );
1980
- } catch (err) {
1981
- throw new T2000Error(
1982
- "PROTOCOL_UNAVAILABLE",
1983
- `Failed to initialize Suilend: ${err instanceof Error ? err.message : String(err)}`
1984
- );
1971
+ // -- Swap --
1972
+ async swap(params) {
1973
+ const fromAsset = params.from.toUpperCase();
1974
+ const toAsset = params.to.toUpperCase();
1975
+ if (!(fromAsset in SUPPORTED_ASSETS) || !(toAsset in SUPPORTED_ASSETS)) {
1976
+ throw new T2000Error("ASSET_NOT_SUPPORTED", `Swap pair ${fromAsset}/${toAsset} is not supported`);
1985
1977
  }
1986
- this.initialized = true;
1987
- }
1988
- ensureInit() {
1989
- if (!this.initialized) {
1990
- throw new T2000Error(
1991
- "PROTOCOL_UNAVAILABLE",
1992
- "SuilendAdapter not initialized. Call init() first."
1993
- );
1978
+ if (fromAsset === toAsset) {
1979
+ throw new T2000Error("INVALID_AMOUNT", "Cannot swap same asset");
1994
1980
  }
1995
- }
1996
- findReserve(asset) {
1997
- const upper = asset.toUpperCase();
1998
- let coinType;
1999
- if (upper === "USDC") coinType = USDC_TYPE2;
2000
- else if (upper === "SUI") coinType = "0x2::sui::SUI";
2001
- else if (asset.includes("::")) coinType = asset;
2002
- else return void 0;
2003
- try {
2004
- const normalized = normalizeStructTag(coinType);
2005
- return this.suilend.lendingMarket.reserves.find(
2006
- (r) => normalizeStructTag(r.coinType.name) === normalized
2007
- );
2008
- } catch {
2009
- return void 0;
1981
+ let adapter;
1982
+ if (params.protocol) {
1983
+ const found = this.registry.getSwap(params.protocol);
1984
+ if (!found) throw new T2000Error("ASSET_NOT_SUPPORTED", `Swap adapter '${params.protocol}' not found`);
1985
+ adapter = found;
1986
+ } else {
1987
+ const best = await this.registry.bestSwapQuote(fromAsset, toAsset, params.amount);
1988
+ adapter = best.adapter;
2010
1989
  }
2011
- }
2012
- async getObligationCaps(address) {
2013
- const SuilendClientStatic = (await import('@suilend/sdk')).SuilendClient;
2014
- return SuilendClientStatic.getObligationOwnerCaps(
2015
- address,
2016
- [this.lendingMarketType],
2017
- this.client
2018
- );
2019
- }
2020
- resolveSymbol(coinType) {
2021
- const normalized = normalizeStructTag(coinType);
2022
- if (normalized === normalizeStructTag(USDC_TYPE2)) return "USDC";
2023
- if (normalized === normalizeStructTag("0x2::sui::SUI")) return "SUI";
2024
- const parts = coinType.split("::");
2025
- return parts[parts.length - 1] || "UNKNOWN";
2026
- }
2027
- async getRates(asset) {
2028
- this.ensureInit();
2029
- const reserve = this.findReserve(asset);
2030
- if (!reserve) {
2031
- throw new T2000Error("ASSET_NOT_SUPPORTED", `Suilend does not support ${asset}`);
1990
+ const fee = calculateFee("swap", params.amount);
1991
+ const swapAmount = params.amount;
1992
+ const slippageBps = params.maxSlippage ? params.maxSlippage * 100 : void 0;
1993
+ let swapMeta = { estimatedOut: 0, toDecimals: 0 };
1994
+ const gasResult = await executeWithGas(this.client, this.keypair, async () => {
1995
+ const built = await adapter.buildSwapTx(this._address, fromAsset, toAsset, swapAmount, slippageBps);
1996
+ swapMeta = { estimatedOut: built.estimatedOut, toDecimals: built.toDecimals };
1997
+ return built.tx;
1998
+ });
1999
+ const toInfo = SUPPORTED_ASSETS[toAsset];
2000
+ await this.client.waitForTransaction({ digest: gasResult.digest });
2001
+ const txDetail = await this.client.getTransactionBlock({
2002
+ digest: gasResult.digest,
2003
+ options: { showBalanceChanges: true }
2004
+ });
2005
+ let actualReceived = 0;
2006
+ if (txDetail.balanceChanges) {
2007
+ for (const change of txDetail.balanceChanges) {
2008
+ if (change.coinType === toInfo.type && change.owner && typeof change.owner === "object" && "AddressOwner" in change.owner && change.owner.AddressOwner === this._address) {
2009
+ const amt = Number(change.amount) / 10 ** toInfo.decimals;
2010
+ if (amt > 0) actualReceived += amt;
2011
+ }
2012
+ }
2032
2013
  }
2033
- const { borrowAprPct, depositAprPct } = computeRatesFromReserve(reserve);
2014
+ const expectedOutput = swapMeta.estimatedOut / 10 ** swapMeta.toDecimals;
2015
+ if (actualReceived === 0) actualReceived = expectedOutput;
2016
+ const priceImpact = expectedOutput > 0 ? Math.abs(actualReceived - expectedOutput) / expectedOutput : 0;
2017
+ reportFee(this._address, "swap", fee.amount, fee.rate, gasResult.digest);
2018
+ this.emitBalanceChange(fromAsset, swapAmount, "swap", gasResult.digest);
2034
2019
  return {
2035
- asset,
2036
- saveApy: depositAprPct,
2037
- borrowApy: borrowAprPct
2020
+ success: true,
2021
+ tx: gasResult.digest,
2022
+ fromAmount: swapAmount,
2023
+ fromAsset,
2024
+ toAmount: actualReceived,
2025
+ toAsset,
2026
+ priceImpact,
2027
+ fee: fee.amount,
2028
+ gasCost: gasResult.gasCostSui,
2029
+ gasMethod: gasResult.gasMethod
2038
2030
  };
2039
2031
  }
2040
- async getPositions(address) {
2041
- this.ensureInit();
2042
- const supplies = [];
2043
- const borrows = [];
2044
- const caps = await this.getObligationCaps(address);
2045
- if (caps.length === 0) return { supplies, borrows };
2046
- const obligation = await this.suilend.getObligation(caps[0].obligationId);
2047
- for (const deposit of obligation.deposits) {
2048
- const coinType = normalizeStructTag(deposit.coinType.name);
2049
- const reserve = this.suilend.lendingMarket.reserves.find(
2050
- (r) => normalizeStructTag(r.coinType.name) === coinType
2051
- );
2052
- if (!reserve) continue;
2053
- const ctokenAmount = Number(deposit.depositedCtokenAmount.toString());
2054
- const ratio = cTokenRatio(reserve);
2055
- const amount = ctokenAmount * ratio / 10 ** reserve.mintDecimals;
2056
- const { depositAprPct } = computeRatesFromReserve(reserve);
2057
- supplies.push({ asset: this.resolveSymbol(coinType), amount, apy: depositAprPct });
2058
- }
2059
- for (const borrow of obligation.borrows) {
2060
- const coinType = normalizeStructTag(borrow.coinType.name);
2061
- const reserve = this.suilend.lendingMarket.reserves.find(
2062
- (r) => normalizeStructTag(r.coinType.name) === coinType
2063
- );
2064
- if (!reserve) continue;
2065
- const rawBorrowed = Number(borrow.borrowedAmount.value.toString()) / WAD;
2066
- const amount = rawBorrowed / 10 ** reserve.mintDecimals;
2067
- const reserveRate = Number(reserve.cumulativeBorrowRate.value.toString()) / WAD;
2068
- const posRate = Number(borrow.cumulativeBorrowRate.value.toString()) / WAD;
2069
- const compounded = posRate > 0 ? amount * (reserveRate / posRate) : amount;
2070
- const { borrowAprPct } = computeRatesFromReserve(reserve);
2071
- borrows.push({ asset: this.resolveSymbol(coinType), amount: compounded, apy: borrowAprPct });
2072
- }
2073
- return { supplies, borrows };
2032
+ async swapQuote(params) {
2033
+ const fromAsset = params.from.toUpperCase();
2034
+ const toAsset = params.to.toUpperCase();
2035
+ const best = await this.registry.bestSwapQuote(fromAsset, toAsset, params.amount);
2036
+ const fee = calculateFee("swap", params.amount);
2037
+ return { ...best.quote, fee: { amount: fee.amount, rate: fee.rate } };
2038
+ }
2039
+ // -- Info --
2040
+ async positions() {
2041
+ const allPositions = await this.registry.allPositions(this._address);
2042
+ const positions = allPositions.flatMap(
2043
+ (p) => [
2044
+ ...p.positions.supplies.map((s) => ({
2045
+ protocol: p.protocolId,
2046
+ asset: s.asset,
2047
+ type: "save",
2048
+ amount: s.amount,
2049
+ apy: s.apy
2050
+ })),
2051
+ ...p.positions.borrows.map((b) => ({
2052
+ protocol: p.protocolId,
2053
+ asset: b.asset,
2054
+ type: "borrow",
2055
+ amount: b.amount,
2056
+ apy: b.apy
2057
+ }))
2058
+ ]
2059
+ );
2060
+ return { positions };
2074
2061
  }
2075
- async getHealth(address) {
2076
- this.ensureInit();
2077
- const caps = await this.getObligationCaps(address);
2078
- if (caps.length === 0) {
2079
- return { healthFactor: Infinity, supplied: 0, borrowed: 0, maxBorrow: 0, liquidationThreshold: 0 };
2062
+ async rates() {
2063
+ return getRates(this.client);
2064
+ }
2065
+ async allRates(asset = "USDC") {
2066
+ return this.registry.allRates(asset);
2067
+ }
2068
+ async earnings() {
2069
+ const result = await getEarnings(this.client, this.keypair);
2070
+ if (result.totalYieldEarned > 0) {
2071
+ this.emit("yield", {
2072
+ earned: result.dailyEarning,
2073
+ total: result.totalYieldEarned,
2074
+ apy: result.currentApy / 100,
2075
+ timestamp: Date.now()
2076
+ });
2080
2077
  }
2081
- const positions = await this.getPositions(address);
2082
- const supplied = positions.supplies.reduce((s, p) => s + p.amount, 0);
2083
- const borrowed = positions.borrows.reduce((s, p) => s + p.amount, 0);
2084
- const reserve = this.findReserve("USDC");
2085
- const closeLtv = reserve?.config?.element?.closeLtvPct ?? 75;
2086
- const openLtv = reserve?.config?.element?.openLtvPct ?? 70;
2087
- const liqThreshold = closeLtv / 100;
2088
- const healthFactor = borrowed > 0 ? supplied * liqThreshold / borrowed : Infinity;
2089
- const maxBorrow = Math.max(0, supplied * (openLtv / 100) - borrowed);
2090
- return { healthFactor, supplied, borrowed, maxBorrow, liquidationThreshold: liqThreshold };
2078
+ return result;
2091
2079
  }
2092
- async buildSaveTx(address, amount, _asset, options) {
2093
- this.ensureInit();
2094
- const rawAmount = usdcToRaw(amount).toString();
2095
- const tx = new Transaction();
2096
- tx.setSender(address);
2097
- const caps = await this.getObligationCaps(address);
2098
- let capRef;
2099
- if (caps.length === 0) {
2100
- const [newCap] = this.suilend.createObligation(tx);
2101
- capRef = newCap;
2102
- } else {
2103
- capRef = caps[0].id;
2080
+ async fundStatus() {
2081
+ return getFundStatus(this.client, this.keypair);
2082
+ }
2083
+ // -- Sentinel --
2084
+ async sentinelList() {
2085
+ return listSentinels();
2086
+ }
2087
+ async sentinelInfo(id) {
2088
+ return getSentinelInfo(this.client, id);
2089
+ }
2090
+ async sentinelAttack(id, prompt, fee) {
2091
+ return attack(this.client, this.keypair, id, prompt, fee);
2092
+ }
2093
+ // -- Helpers --
2094
+ async resolveLending(protocol, asset, capability) {
2095
+ if (protocol) {
2096
+ const adapter = this.registry.getLending(protocol);
2097
+ if (!adapter) throw new T2000Error("ASSET_NOT_SUPPORTED", `Lending adapter '${protocol}' not found`);
2098
+ return adapter;
2104
2099
  }
2105
- const allCoins = await this.fetchAllCoins(address, USDC_TYPE2);
2106
- if (allCoins.length === 0) {
2107
- throw new T2000Error("INSUFFICIENT_BALANCE", "No USDC coins found");
2100
+ if (capability === "save") {
2101
+ const { adapter } = await this.registry.bestSaveRate(asset);
2102
+ return adapter;
2108
2103
  }
2109
- const primaryCoinId = allCoins[0].coinObjectId;
2110
- if (allCoins.length > 1) {
2111
- tx.mergeCoins(
2112
- tx.object(primaryCoinId),
2113
- allCoins.slice(1).map((c) => tx.object(c.coinObjectId))
2104
+ if (capability === "borrow" || capability === "repay") {
2105
+ const adapters2 = this.registry.listLending().filter(
2106
+ (a) => a.supportedAssets.includes(asset) && a.capabilities.includes(capability) && (capability !== "borrow" || a.supportsSameAssetBorrow)
2114
2107
  );
2108
+ if (adapters2.length === 0) throw new T2000Error("ASSET_NOT_SUPPORTED", `No adapter supports ${capability} ${asset}`);
2109
+ return adapters2[0];
2115
2110
  }
2116
- const [depositCoin] = tx.splitCoins(tx.object(primaryCoinId), [rawAmount]);
2117
- if (options?.collectFee) {
2118
- addCollectFeeToTx(tx, depositCoin, "save");
2119
- }
2120
- this.suilend.deposit(depositCoin, USDC_TYPE2, capRef, tx);
2121
- return { tx };
2122
- }
2123
- async buildWithdrawTx(address, amount, _asset) {
2124
- this.ensureInit();
2125
- const caps = await this.getObligationCaps(address);
2126
- if (caps.length === 0) {
2127
- throw new T2000Error("NO_COLLATERAL", "No Suilend position found");
2128
- }
2129
- const positions = await this.getPositions(address);
2130
- const usdcSupply = positions.supplies.find((s) => s.asset === "USDC");
2131
- const deposited = usdcSupply?.amount ?? 0;
2132
- const effectiveAmount = Math.min(amount, deposited);
2133
- if (effectiveAmount <= 0) {
2134
- throw new T2000Error("NO_COLLATERAL", "Nothing to withdraw from Suilend");
2135
- }
2136
- const rawAmount = usdcToRaw(effectiveAmount).toString();
2137
- const tx = new Transaction();
2138
- tx.setSender(address);
2139
- await this.suilend.withdrawAndSendToUser(
2140
- address,
2141
- caps[0].id,
2142
- caps[0].obligationId,
2143
- USDC_TYPE2,
2144
- rawAmount,
2145
- tx
2111
+ const adapters = this.registry.listLending().filter(
2112
+ (a) => a.supportedAssets.includes(asset) && a.capabilities.includes(capability)
2146
2113
  );
2147
- return { tx, effectiveAmount };
2114
+ if (adapters.length === 0) throw new T2000Error("ASSET_NOT_SUPPORTED", `No adapter supports ${capability} ${asset}`);
2115
+ return adapters[0];
2148
2116
  }
2149
- async buildBorrowTx(_address, _amount, _asset, _options) {
2150
- throw new T2000Error(
2151
- "ASSET_NOT_SUPPORTED",
2152
- "SuilendAdapter.buildBorrowTx() not available \u2014 Suilend requires different collateral/borrow assets. Deferred to Phase 10."
2153
- );
2117
+ emitBalanceChange(asset, amount, cause, tx) {
2118
+ this.emit("balanceChange", { asset, previous: 0, current: 0, cause, tx });
2154
2119
  }
2155
- async buildRepayTx(_address, _amount, _asset) {
2156
- throw new T2000Error(
2157
- "ASSET_NOT_SUPPORTED",
2158
- "SuilendAdapter.buildRepayTx() not available \u2014 deferred to Phase 10."
2159
- );
2120
+ };
2121
+ async function callSponsorApi(address, name) {
2122
+ const res = await fetch(`${API_BASE_URL}/api/sponsor`, {
2123
+ method: "POST",
2124
+ headers: { "Content-Type": "application/json" },
2125
+ body: JSON.stringify({ address, name })
2126
+ });
2127
+ if (res.status === 429) {
2128
+ const data = await res.json();
2129
+ if (data.challenge) {
2130
+ const proof = solveHashcash(data.challenge);
2131
+ const retry = await fetch(`${API_BASE_URL}/api/sponsor`, {
2132
+ method: "POST",
2133
+ headers: { "Content-Type": "application/json" },
2134
+ body: JSON.stringify({ address, name, proof })
2135
+ });
2136
+ if (!retry.ok) throw new T2000Error("SPONSOR_RATE_LIMITED", "Sponsor rate limited");
2137
+ return;
2138
+ }
2160
2139
  }
2161
- async maxWithdraw(address, _asset) {
2162
- this.ensureInit();
2163
- const health = await this.getHealth(address);
2164
- let maxAmount;
2165
- if (health.borrowed === 0) {
2166
- maxAmount = health.supplied;
2167
- } else {
2168
- maxAmount = Math.max(
2169
- 0,
2170
- health.supplied - health.borrowed * MIN_HEALTH_FACTOR2 / health.liquidationThreshold
2171
- );
2140
+ if (!res.ok) {
2141
+ throw new T2000Error("SPONSOR_FAILED", "Sponsor API unavailable");
2142
+ }
2143
+ }
2144
+
2145
+ // src/utils/simulate.ts
2146
+ async function simulateTransaction(client, tx, sender) {
2147
+ tx.setSender(sender);
2148
+ try {
2149
+ const txBytes = await tx.build({ client });
2150
+ const dryRun = await client.dryRunTransactionBlock({
2151
+ transactionBlock: Buffer.from(txBytes).toString("base64")
2152
+ });
2153
+ const status = dryRun.effects?.status;
2154
+ const gasUsed = dryRun.effects?.gasUsed;
2155
+ const gasEstimateSui = gasUsed ? (Number(gasUsed.computationCost) + Number(gasUsed.storageCost) - Number(gasUsed.storageRebate)) / 1e9 : 0;
2156
+ if (status?.status === "failure") {
2157
+ const rawError = status.error ?? "Unknown simulation error";
2158
+ const parsed = parseMoveAbort(rawError);
2159
+ return {
2160
+ success: false,
2161
+ gasEstimateSui,
2162
+ error: {
2163
+ moveAbortCode: parsed.abortCode,
2164
+ moveModule: parsed.module,
2165
+ reason: parsed.reason,
2166
+ rawError
2167
+ }
2168
+ };
2172
2169
  }
2173
- const remainingSupply = health.supplied - maxAmount;
2174
- const hfAfter = health.borrowed > 0 ? remainingSupply * health.liquidationThreshold / health.borrowed : Infinity;
2170
+ return { success: true, gasEstimateSui };
2171
+ } catch (err) {
2172
+ const rawError = err instanceof Error ? err.message : String(err);
2175
2173
  return {
2176
- maxAmount,
2177
- healthFactorAfter: hfAfter,
2178
- currentHF: health.healthFactor
2174
+ success: false,
2175
+ gasEstimateSui: 0,
2176
+ error: {
2177
+ reason: "Simulation failed: " + rawError,
2178
+ rawError
2179
+ }
2179
2180
  };
2180
2181
  }
2181
- async maxBorrow(_address, _asset) {
2182
- throw new T2000Error(
2183
- "ASSET_NOT_SUPPORTED",
2184
- "SuilendAdapter.maxBorrow() not available \u2014 deferred to Phase 10."
2185
- );
2186
- }
2187
- async fetchAllCoins(owner, coinType) {
2188
- const all = [];
2189
- let cursor = null;
2190
- let hasNext = true;
2191
- while (hasNext) {
2192
- const page = await this.client.getCoins({ owner, coinType, cursor: cursor ?? void 0 });
2193
- all.push(...page.data.map((c) => ({ coinObjectId: c.coinObjectId, balance: c.balance })));
2194
- cursor = page.nextCursor;
2195
- hasNext = page.hasNextPage;
2182
+ }
2183
+ function throwIfSimulationFailed(sim) {
2184
+ if (sim.success) return;
2185
+ throw new T2000Error(
2186
+ "SIMULATION_FAILED",
2187
+ sim.error?.reason ?? "Transaction simulation failed",
2188
+ {
2189
+ moveAbortCode: sim.error?.moveAbortCode,
2190
+ moveModule: sim.error?.moveModule,
2191
+ reason: sim.error?.reason,
2192
+ rawError: sim.error?.rawError
2196
2193
  }
2197
- return all;
2194
+ );
2195
+ }
2196
+ function parseMoveAbort(errorStr) {
2197
+ const abortMatch = errorStr.match(/MoveAbort\([^,]*,\s*(\d+)\)/);
2198
+ const moduleMatch = errorStr.match(/name:\s*Identifier\("([^"]+)"\)/);
2199
+ if (abortMatch) {
2200
+ const code = parseInt(abortMatch[1], 10);
2201
+ const module = moduleMatch?.[1];
2202
+ const reason = mapMoveAbortCode(code);
2203
+ return { abortCode: code, module, reason };
2198
2204
  }
2199
- };
2205
+ if (errorStr.includes("MovePrimitiveRuntimeError")) {
2206
+ const module = moduleMatch?.[1];
2207
+ return {
2208
+ module,
2209
+ reason: `Move runtime error in ${module ?? "unknown"} module`
2210
+ };
2211
+ }
2212
+ return { reason: errorStr };
2213
+ }
2200
2214
 
2201
2215
  export { BPS_DENOMINATOR, CLOCK_ID, CetusAdapter, DEFAULT_NETWORK, MIST_PER_SUI, NaviAdapter, ProtocolRegistry, SENTINEL, SUI_DECIMALS, SUPPORTED_ASSETS, SuilendAdapter, T2000, T2000Error, USDC_DECIMALS, addCollectFeeToTx, calculateFee, executeAutoTopUp, executeWithGas, exportPrivateKey, formatSui, formatUsd, generateKeypair, getAddress, getGasStatus, getPoolPrice, getRates, getSentinelInfo, getSwapQuote, keypairFromPrivateKey, listSentinels, loadKey, mapMoveAbortCode, mapWalletError, mistToSui, rawToUsdc, requestAttack, saveKey, attack as sentinelAttack, settleAttack, shouldAutoTopUp, simulateTransaction, solveHashcash, submitPrompt, suiToMist, throwIfSimulationFailed, truncateAddress, usdcToRaw, validateAddress, walletExists };
2202
2216
  //# sourceMappingURL=index.js.map