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