@t2000/sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1,1551 @@
1
+ 'use strict';
2
+
3
+ var eventemitter3 = require('eventemitter3');
4
+ var client = require('@mysten/sui/client');
5
+ var utils = require('@mysten/sui/utils');
6
+ var ed25519 = require('@mysten/sui/keypairs/ed25519');
7
+ var cryptography = require('@mysten/sui/cryptography');
8
+ var crypto = require('crypto');
9
+ var promises = require('fs/promises');
10
+ var path = require('path');
11
+ var os = require('os');
12
+ var transactions = require('@mysten/sui/transactions');
13
+ var lending = require('@naviprotocol/lending');
14
+ var suiClmmSdk = require('@cetusprotocol/sui-clmm-sdk');
15
+
16
+ // src/t2000.ts
17
+
18
+ // src/constants.ts
19
+ var MIST_PER_SUI = 1000000000n;
20
+ var SUI_DECIMALS = 9;
21
+ var USDC_DECIMALS = 6;
22
+ var BPS_DENOMINATOR = 10000n;
23
+ var AUTO_TOPUP_THRESHOLD = 50000000n;
24
+ var AUTO_TOPUP_AMOUNT = 1000000n;
25
+ var AUTO_TOPUP_MIN_USDC = 5000000n;
26
+ var SAVE_FEE_BPS = 10n;
27
+ var SWAP_FEE_BPS = 10n;
28
+ var BORROW_FEE_BPS = 5n;
29
+ var CLOCK_ID = "0x6";
30
+ var SUPPORTED_ASSETS = {
31
+ USDC: {
32
+ type: "0xdba34672e30cb065b1f93e3ab55318768fd6fef66c15942c9f7cb846e2f900e7::usdc::USDC",
33
+ decimals: 6,
34
+ symbol: "USDC"
35
+ },
36
+ SUI: {
37
+ type: "0x2::sui::SUI",
38
+ decimals: 9,
39
+ symbol: "SUI"
40
+ }
41
+ };
42
+ process.env.T2000_PACKAGE_ID ?? "0x51c44bb2ad3ba608cf9adbc6e37ee67268ef9313a4ff70957d4c6e7955dc7eef";
43
+ process.env.T2000_CONFIG_ID ?? "0xd30408960ac38eced670acc102df9e178b5b46b3a8c0e96a53ec2fd3f39b5936";
44
+ process.env.T2000_TREASURY_ID ?? "0x2398c2759cfce40f1b0f2b3e524eeba9e8f6428fcb1d1e39235dd042d48defc8";
45
+ var DEFAULT_NETWORK = "mainnet";
46
+ var DEFAULT_RPC_URL = "https://fullnode.mainnet.sui.io:443";
47
+ var DEFAULT_KEY_PATH = "~/.t2000/wallet.key";
48
+ var API_BASE_URL = process.env.T2000_API_URL ?? "https://api.t2000.ai";
49
+ var CETUS_USDC_SUI_POOL = "0xb8d7d9e66a60c239e7a60110efcf8b555571a820a5c015ae1ce01bd5e9c4ac51";
50
+ var CETUS_GLOBAL_CONFIG = "0xdaa46292632c3c4d8f31f23ea0f9b36a28ff3677e9684980e4438403a67a3d8f";
51
+ var CETUS_PACKAGE = "0x1eabed72c53feb3805120a081dc15963c204dc8d091542592abaf7a35689b2fb";
52
+
53
+ // src/errors.ts
54
+ var T2000Error = class extends Error {
55
+ code;
56
+ data;
57
+ retryable;
58
+ constructor(code, message, data, retryable = false) {
59
+ super(message);
60
+ this.name = "T2000Error";
61
+ this.code = code;
62
+ this.data = data;
63
+ this.retryable = retryable;
64
+ }
65
+ toJSON() {
66
+ return {
67
+ error: this.code,
68
+ message: this.message,
69
+ ...this.data && { data: this.data },
70
+ retryable: this.retryable
71
+ };
72
+ }
73
+ };
74
+ function mapWalletError(error) {
75
+ const msg = error instanceof Error ? error.message : String(error);
76
+ if (msg.includes("rejected") || msg.includes("cancelled")) {
77
+ return new T2000Error("TRANSACTION_FAILED", "Transaction cancelled");
78
+ }
79
+ if (msg.includes("Insufficient") || msg.includes("insufficient")) {
80
+ return new T2000Error("INSUFFICIENT_BALANCE", "Insufficient balance");
81
+ }
82
+ return new T2000Error("UNKNOWN", msg, void 0, true);
83
+ }
84
+ function mapMoveAbortCode(code) {
85
+ const abortMessages = {
86
+ 1: "Protocol is temporarily paused",
87
+ 2: "Amount must be greater than zero",
88
+ 3: "Invalid operation type",
89
+ 4: "Fee rate exceeds maximum",
90
+ 5: "Insufficient treasury balance",
91
+ 6: "Not authorized",
92
+ 7: "Package version mismatch \u2014 upgrade required",
93
+ 8: "Timelock is active \u2014 wait for expiry",
94
+ 9: "No pending change to execute"
95
+ };
96
+ return abortMessages[code] ?? `Move abort code: ${code}`;
97
+ }
98
+
99
+ // src/utils/sui.ts
100
+ var cachedClient = null;
101
+ function getSuiClient(rpcUrl) {
102
+ const url = rpcUrl ?? DEFAULT_RPC_URL;
103
+ if (cachedClient) return cachedClient;
104
+ cachedClient = new client.SuiClient({ url });
105
+ return cachedClient;
106
+ }
107
+ function validateAddress(address) {
108
+ const normalized = utils.normalizeSuiAddress(address);
109
+ if (!utils.isValidSuiAddress(normalized)) {
110
+ throw new T2000Error("INVALID_ADDRESS", `Invalid Sui address: ${address}`);
111
+ }
112
+ return normalized;
113
+ }
114
+ function truncateAddress(address) {
115
+ if (address.length <= 10) return address;
116
+ return `${address.slice(0, 6)}...${address.slice(-4)}`;
117
+ }
118
+ var ALGORITHM = "aes-256-gcm";
119
+ var SCRYPT_N = 2 ** 14;
120
+ var SCRYPT_R = 8;
121
+ var SCRYPT_P = 1;
122
+ var SALT_LENGTH = 32;
123
+ var IV_LENGTH = 16;
124
+ function expandPath(p) {
125
+ if (p.startsWith("~")) return path.resolve(os.homedir(), p.slice(2));
126
+ return path.resolve(p);
127
+ }
128
+ function deriveKey(passphrase, salt) {
129
+ return crypto.scryptSync(passphrase, salt, 32, { N: SCRYPT_N, r: SCRYPT_R, p: SCRYPT_P });
130
+ }
131
+ function encrypt(data, passphrase) {
132
+ const salt = crypto.randomBytes(SALT_LENGTH);
133
+ const key = deriveKey(passphrase, salt);
134
+ const iv = crypto.randomBytes(IV_LENGTH);
135
+ const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
136
+ const ciphertext = Buffer.concat([cipher.update(data), cipher.final()]);
137
+ const tag = cipher.getAuthTag();
138
+ return {
139
+ version: 1,
140
+ algorithm: ALGORITHM,
141
+ salt: salt.toString("hex"),
142
+ iv: iv.toString("hex"),
143
+ tag: tag.toString("hex"),
144
+ ciphertext: ciphertext.toString("hex")
145
+ };
146
+ }
147
+ function decrypt(encrypted, passphrase) {
148
+ const salt = Buffer.from(encrypted.salt, "hex");
149
+ const key = deriveKey(passphrase, salt);
150
+ const iv = Buffer.from(encrypted.iv, "hex");
151
+ const tag = Buffer.from(encrypted.tag, "hex");
152
+ const ciphertext = Buffer.from(encrypted.ciphertext, "hex");
153
+ const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
154
+ decipher.setAuthTag(tag);
155
+ try {
156
+ return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
157
+ } catch {
158
+ throw new T2000Error("WALLET_LOCKED", "Invalid passphrase");
159
+ }
160
+ }
161
+ function generateKeypair() {
162
+ return ed25519.Ed25519Keypair.generate();
163
+ }
164
+ function keypairFromPrivateKey(privateKey) {
165
+ if (privateKey.startsWith("suiprivkey")) {
166
+ const decoded = cryptography.decodeSuiPrivateKey(privateKey);
167
+ return ed25519.Ed25519Keypair.fromSecretKey(decoded.secretKey);
168
+ }
169
+ const bytes = Buffer.from(privateKey.replace(/^0x/, ""), "hex");
170
+ return ed25519.Ed25519Keypair.fromSecretKey(bytes);
171
+ }
172
+ async function saveKey(keypair, passphrase, keyPath) {
173
+ const filePath = expandPath(keyPath ?? DEFAULT_KEY_PATH);
174
+ try {
175
+ await promises.access(filePath);
176
+ throw new T2000Error("WALLET_EXISTS", `Wallet already exists at ${filePath}`);
177
+ } catch (error) {
178
+ if (error instanceof T2000Error) throw error;
179
+ }
180
+ await promises.mkdir(path.dirname(filePath), { recursive: true });
181
+ const bech32Key = keypair.getSecretKey();
182
+ const encrypted = encrypt(Buffer.from(bech32Key, "utf-8"), passphrase);
183
+ await promises.writeFile(filePath, JSON.stringify(encrypted, null, 2), { mode: 384 });
184
+ return filePath;
185
+ }
186
+ async function loadKey(passphrase, keyPath) {
187
+ const filePath = expandPath(keyPath ?? DEFAULT_KEY_PATH);
188
+ let content;
189
+ try {
190
+ content = await promises.readFile(filePath, "utf-8");
191
+ } catch {
192
+ throw new T2000Error("WALLET_NOT_FOUND", `No wallet found at ${filePath}`);
193
+ }
194
+ const encrypted = JSON.parse(content);
195
+ const decrypted = decrypt(encrypted, passphrase);
196
+ const bech32Key = decrypted.toString("utf-8");
197
+ const decoded = cryptography.decodeSuiPrivateKey(bech32Key);
198
+ return ed25519.Ed25519Keypair.fromSecretKey(decoded.secretKey);
199
+ }
200
+ async function walletExists(keyPath) {
201
+ const filePath = expandPath(keyPath ?? DEFAULT_KEY_PATH);
202
+ try {
203
+ await promises.access(filePath);
204
+ return true;
205
+ } catch {
206
+ return false;
207
+ }
208
+ }
209
+ function exportPrivateKey(keypair) {
210
+ return keypair.getSecretKey();
211
+ }
212
+ function getAddress(keypair) {
213
+ return keypair.getPublicKey().toSuiAddress();
214
+ }
215
+
216
+ // src/utils/format.ts
217
+ function mistToSui(mist) {
218
+ return Number(mist) / Number(MIST_PER_SUI);
219
+ }
220
+ function suiToMist(sui) {
221
+ return BigInt(Math.round(sui * Number(MIST_PER_SUI)));
222
+ }
223
+ function usdcToRaw(amount) {
224
+ return BigInt(Math.round(amount * 10 ** USDC_DECIMALS));
225
+ }
226
+ function rawToUsdc(raw) {
227
+ return Number(raw) / 10 ** USDC_DECIMALS;
228
+ }
229
+ function displayToRaw(amount, decimals) {
230
+ return BigInt(Math.round(amount * 10 ** decimals));
231
+ }
232
+ function formatUsd(amount) {
233
+ return `$${amount.toFixed(2)}`;
234
+ }
235
+ function formatSui(amount) {
236
+ if (amount < 1e-3) return `${amount.toFixed(6)} SUI`;
237
+ return `${amount.toFixed(3)} SUI`;
238
+ }
239
+
240
+ // src/wallet/send.ts
241
+ async function buildAndExecuteSend({
242
+ client,
243
+ keypair,
244
+ to,
245
+ amount,
246
+ asset = "USDC"
247
+ }) {
248
+ const recipient = validateAddress(to);
249
+ const assetInfo = SUPPORTED_ASSETS[asset];
250
+ if (!assetInfo) {
251
+ throw new T2000Error("ASSET_NOT_SUPPORTED", `Asset ${asset} is not supported`);
252
+ }
253
+ if (amount <= 0) {
254
+ throw new T2000Error("INVALID_AMOUNT", "Amount must be greater than zero");
255
+ }
256
+ const senderAddress = keypair.getPublicKey().toSuiAddress();
257
+ const rawAmount = displayToRaw(amount, assetInfo.decimals);
258
+ const tx = new transactions.Transaction();
259
+ if (asset === "SUI") {
260
+ const [coin] = tx.splitCoins(tx.gas, [rawAmount]);
261
+ tx.transferObjects([coin], recipient);
262
+ } else {
263
+ const coins = await client.getCoins({
264
+ owner: senderAddress,
265
+ coinType: assetInfo.type
266
+ });
267
+ if (coins.data.length === 0) {
268
+ throw new T2000Error("INSUFFICIENT_BALANCE", `No ${asset} coins found`);
269
+ }
270
+ const totalBalance = coins.data.reduce(
271
+ (sum, c) => sum + BigInt(c.balance),
272
+ 0n
273
+ );
274
+ if (totalBalance < rawAmount) {
275
+ throw new T2000Error("INSUFFICIENT_BALANCE", `Insufficient ${asset} balance`, {
276
+ available: Number(totalBalance) / 10 ** assetInfo.decimals,
277
+ required: amount
278
+ });
279
+ }
280
+ const primaryCoin = tx.object(coins.data[0].coinObjectId);
281
+ if (coins.data.length > 1) {
282
+ tx.mergeCoins(
283
+ primaryCoin,
284
+ coins.data.slice(1).map((c) => tx.object(c.coinObjectId))
285
+ );
286
+ }
287
+ const [sendCoin] = tx.splitCoins(primaryCoin, [rawAmount]);
288
+ tx.transferObjects([sendCoin], recipient);
289
+ }
290
+ const result = await client.signAndExecuteTransaction({
291
+ signer: keypair,
292
+ transaction: tx,
293
+ options: { showEffects: true }
294
+ });
295
+ await client.waitForTransaction({ digest: result.digest });
296
+ const gasUsed = result.effects?.gasUsed;
297
+ const gasCost = gasUsed ? Math.abs(
298
+ (Number(gasUsed.computationCost) + Number(gasUsed.storageCost) - Number(gasUsed.storageRebate)) / 1e9
299
+ ) : 0;
300
+ return {
301
+ digest: result.digest,
302
+ gasCost
303
+ };
304
+ }
305
+
306
+ // src/wallet/balance.ts
307
+ var _cachedSuiPrice = 3.5;
308
+ var _priceLastFetched = 0;
309
+ var PRICE_CACHE_TTL_MS = 6e4;
310
+ async function fetchSuiPrice(client) {
311
+ const now = Date.now();
312
+ if (now - _priceLastFetched < PRICE_CACHE_TTL_MS) return _cachedSuiPrice;
313
+ try {
314
+ const pool = await client.getObject({
315
+ id: CETUS_USDC_SUI_POOL,
316
+ options: { showContent: true }
317
+ });
318
+ if (pool.data?.content?.dataType === "moveObject") {
319
+ const fields = pool.data.content.fields;
320
+ const currentSqrtPrice = BigInt(String(fields.current_sqrt_price ?? "0"));
321
+ if (currentSqrtPrice > 0n) {
322
+ const Q64 = 2n ** 64n;
323
+ const sqrtPriceFloat = Number(currentSqrtPrice) / Number(Q64);
324
+ const rawPrice = sqrtPriceFloat * sqrtPriceFloat;
325
+ const price = rawPrice * 1e3;
326
+ if (price > 0.01 && price < 1e3) {
327
+ _cachedSuiPrice = price;
328
+ _priceLastFetched = now;
329
+ }
330
+ }
331
+ }
332
+ } catch {
333
+ }
334
+ return _cachedSuiPrice;
335
+ }
336
+ async function queryBalance(client, address) {
337
+ const [usdcBalance, suiBalance, suiPriceUsd] = await Promise.all([
338
+ client.getBalance({ owner: address, coinType: SUPPORTED_ASSETS.USDC.type }),
339
+ client.getBalance({ owner: address, coinType: SUPPORTED_ASSETS.SUI.type }),
340
+ fetchSuiPrice(client)
341
+ ]);
342
+ const usdcAmount = Number(usdcBalance.totalBalance) / 10 ** SUPPORTED_ASSETS.USDC.decimals;
343
+ const suiAmount = Number(suiBalance.totalBalance) / Number(MIST_PER_SUI);
344
+ const savings = 0;
345
+ const usdEquiv = suiAmount * suiPriceUsd;
346
+ const total = usdcAmount + savings + usdEquiv;
347
+ return {
348
+ available: usdcAmount,
349
+ savings,
350
+ gasReserve: {
351
+ sui: suiAmount,
352
+ usdEquiv
353
+ },
354
+ total,
355
+ assets: {
356
+ USDC: usdcAmount,
357
+ SUI: suiAmount
358
+ }
359
+ };
360
+ }
361
+
362
+ // src/wallet/history.ts
363
+ async function queryHistory(client, address, limit = 20) {
364
+ const txns = await client.queryTransactionBlocks({
365
+ filter: { FromAddress: address },
366
+ options: { showEffects: true, showInput: true },
367
+ limit,
368
+ order: "descending"
369
+ });
370
+ return txns.data.map((tx) => {
371
+ const gasUsed = tx.effects?.gasUsed;
372
+ const gasCost = gasUsed ? (Number(gasUsed.computationCost) + Number(gasUsed.storageCost) - Number(gasUsed.storageRebate)) / 1e9 : void 0;
373
+ return {
374
+ digest: tx.digest,
375
+ action: inferAction(tx.transaction),
376
+ timestamp: Number(tx.timestampMs ?? 0),
377
+ gasCost
378
+ };
379
+ });
380
+ }
381
+ function inferAction(txBlock) {
382
+ if (!txBlock || typeof txBlock !== "object") return "unknown";
383
+ const data = "data" in txBlock ? txBlock.data : void 0;
384
+ if (!data || typeof data !== "object") return "unknown";
385
+ const inner = "transaction" in data ? data.transaction : void 0;
386
+ if (!inner || typeof inner !== "object") return "unknown";
387
+ const kind = "kind" in inner ? inner.kind : void 0;
388
+ if (kind === "ProgrammableTransaction") return "transaction";
389
+ return kind ?? "unknown";
390
+ }
391
+ var ENV = { env: "prod" };
392
+ var USDC_TYPE = SUPPORTED_ASSETS.USDC.type;
393
+ var RATE_DECIMALS = 27;
394
+ var LTV_DECIMALS = 27;
395
+ var MIN_HEALTH_FACTOR = 1.5;
396
+ var WITHDRAW_DUST_BUFFER = 1e-3;
397
+ var NAVI_BALANCE_DECIMALS = 9;
398
+ function clientOpt(client, fresh = false) {
399
+ return { client, ...ENV, ...fresh ? { disableCache: true } : {} };
400
+ }
401
+ function extractGasCost(effects) {
402
+ if (!effects?.gasUsed) return 0;
403
+ return Math.abs(
404
+ (Number(effects.gasUsed.computationCost) + Number(effects.gasUsed.storageCost) - Number(effects.gasUsed.storageRebate)) / 1e9
405
+ );
406
+ }
407
+ function rateToApy(rawRate) {
408
+ if (!rawRate || rawRate === "0") return 0;
409
+ return Number(BigInt(rawRate)) / 10 ** RATE_DECIMALS * 100;
410
+ }
411
+ function parseLtv(rawLtv) {
412
+ if (!rawLtv || rawLtv === "0") return 0.75;
413
+ return Number(BigInt(rawLtv)) / 10 ** LTV_DECIMALS;
414
+ }
415
+ function parseLiqThreshold(val) {
416
+ if (typeof val === "number") return val;
417
+ const n = Number(val);
418
+ if (n > 1) return Number(BigInt(val)) / 10 ** LTV_DECIMALS;
419
+ return n;
420
+ }
421
+ function findUsdcPosition(state) {
422
+ return state.find(
423
+ (p) => p.pool.token.symbol === "USDC" || p.pool.coinType.toLowerCase().includes("usdc")
424
+ );
425
+ }
426
+ async function updateOracle(tx, client, address) {
427
+ try {
428
+ const [feeds, state] = await Promise.all([
429
+ lending.getPriceFeeds(ENV),
430
+ lending.getLendingState(address, clientOpt(client))
431
+ ]);
432
+ const relevant = lending.filterPriceFeeds(feeds, { lendingState: state });
433
+ if (relevant.length > 0) {
434
+ await lending.updateOraclePricesPTB(tx, relevant, { ...ENV, updatePythPriceFeeds: true });
435
+ }
436
+ } catch {
437
+ }
438
+ }
439
+ async function save(client, keypair, amount) {
440
+ const address = keypair.getPublicKey().toSuiAddress();
441
+ const rawAmount = Number(usdcToRaw(amount));
442
+ const coins = await lending.getCoins(address, { coinType: USDC_TYPE, client });
443
+ if (!coins || coins.length === 0) {
444
+ throw new T2000Error("INSUFFICIENT_BALANCE", "No USDC coins found");
445
+ }
446
+ const tx = new transactions.Transaction();
447
+ tx.setSender(address);
448
+ const coinObj = lending.mergeCoinsPTB(tx, coins, { balance: rawAmount });
449
+ await lending.depositCoinPTB(tx, USDC_TYPE, coinObj, ENV);
450
+ const result = await client.signAndExecuteTransaction({
451
+ signer: keypair,
452
+ transaction: tx,
453
+ options: { showEffects: true }
454
+ });
455
+ await client.waitForTransaction({ digest: result.digest });
456
+ const rates = await getRates();
457
+ return {
458
+ success: true,
459
+ tx: result.digest,
460
+ amount,
461
+ apy: rates.USDC.saveApy,
462
+ fee: 0,
463
+ gasCost: extractGasCost(result.effects),
464
+ gasMethod: "self-funded"
465
+ };
466
+ }
467
+ async function withdraw(client, keypair, amount) {
468
+ const address = keypair.getPublicKey().toSuiAddress();
469
+ const state = await lending.getLendingState(address, clientOpt(client, true));
470
+ const usdcPos = findUsdcPosition(state);
471
+ const deposited = usdcPos ? Number(usdcPos.supplyBalance) / 10 ** NAVI_BALANCE_DECIMALS : 0;
472
+ const effectiveAmount = Math.min(amount, Math.max(0, deposited - WITHDRAW_DUST_BUFFER));
473
+ if (effectiveAmount <= 0) throw new T2000Error("NO_COLLATERAL", "Nothing to withdraw");
474
+ const rawAmount = Number(usdcToRaw(effectiveAmount));
475
+ const tx = new transactions.Transaction();
476
+ tx.setSender(address);
477
+ await updateOracle(tx, client, address);
478
+ const withdrawnCoin = await lending.withdrawCoinPTB(tx, USDC_TYPE, rawAmount, ENV);
479
+ tx.transferObjects([withdrawnCoin], address);
480
+ const result = await client.signAndExecuteTransaction({
481
+ signer: keypair,
482
+ transaction: tx,
483
+ options: { showEffects: true }
484
+ });
485
+ await client.waitForTransaction({ digest: result.digest });
486
+ return {
487
+ success: true,
488
+ tx: result.digest,
489
+ amount: effectiveAmount,
490
+ gasCost: extractGasCost(result.effects),
491
+ gasMethod: "self-funded"
492
+ };
493
+ }
494
+ async function borrow(client, keypair, amount) {
495
+ const address = keypair.getPublicKey().toSuiAddress();
496
+ const rawAmount = Number(usdcToRaw(amount));
497
+ const tx = new transactions.Transaction();
498
+ tx.setSender(address);
499
+ await updateOracle(tx, client, address);
500
+ const borrowedCoin = await lending.borrowCoinPTB(tx, USDC_TYPE, rawAmount, ENV);
501
+ tx.transferObjects([borrowedCoin], address);
502
+ const result = await client.signAndExecuteTransaction({
503
+ signer: keypair,
504
+ transaction: tx,
505
+ options: { showEffects: true }
506
+ });
507
+ await client.waitForTransaction({ digest: result.digest });
508
+ const hf = await lending.getHealthFactor(address, clientOpt(client, true));
509
+ return {
510
+ success: true,
511
+ tx: result.digest,
512
+ amount,
513
+ fee: 0,
514
+ healthFactor: hf,
515
+ gasCost: extractGasCost(result.effects),
516
+ gasMethod: "self-funded"
517
+ };
518
+ }
519
+ async function repay(client, keypair, amount) {
520
+ const address = keypair.getPublicKey().toSuiAddress();
521
+ const rawAmount = Number(usdcToRaw(amount));
522
+ const coins = await lending.getCoins(address, { coinType: USDC_TYPE, client });
523
+ if (!coins || coins.length === 0) {
524
+ throw new T2000Error("INSUFFICIENT_BALANCE", "No USDC coins to repay with");
525
+ }
526
+ const tx = new transactions.Transaction();
527
+ tx.setSender(address);
528
+ const coinObj = lending.mergeCoinsPTB(tx, coins, { balance: rawAmount });
529
+ await lending.repayCoinPTB(tx, USDC_TYPE, coinObj, { ...ENV, amount: rawAmount });
530
+ const result = await client.signAndExecuteTransaction({
531
+ signer: keypair,
532
+ transaction: tx,
533
+ options: { showEffects: true }
534
+ });
535
+ await client.waitForTransaction({ digest: result.digest });
536
+ const state = await lending.getLendingState(address, clientOpt(client, true));
537
+ const usdcPos = findUsdcPosition(state);
538
+ const remainingDebt = usdcPos ? Number(usdcPos.borrowBalance) / 10 ** NAVI_BALANCE_DECIMALS : 0;
539
+ return {
540
+ success: true,
541
+ tx: result.digest,
542
+ amount,
543
+ remainingDebt,
544
+ gasCost: extractGasCost(result.effects),
545
+ gasMethod: "self-funded"
546
+ };
547
+ }
548
+ async function getHealthFactor(client, keypair) {
549
+ const address = keypair.getPublicKey().toSuiAddress();
550
+ const [healthFactor, state, pool] = await Promise.all([
551
+ lending.getHealthFactor(address, clientOpt(client, true)),
552
+ lending.getLendingState(address, clientOpt(client, true)),
553
+ lending.getPool(USDC_TYPE, ENV)
554
+ ]);
555
+ const usdcPos = findUsdcPosition(state);
556
+ const supplied = usdcPos ? Number(usdcPos.supplyBalance) / 10 ** NAVI_BALANCE_DECIMALS : 0;
557
+ const borrowed = usdcPos ? Number(usdcPos.borrowBalance) / 10 ** NAVI_BALANCE_DECIMALS : 0;
558
+ const ltv = parseLtv(pool.ltv);
559
+ const liqThreshold = parseLiqThreshold(pool.liquidationFactor.threshold);
560
+ const maxBorrowVal = Math.max(0, supplied * ltv - borrowed);
561
+ return {
562
+ healthFactor: borrowed > 0 ? healthFactor : Infinity,
563
+ supplied,
564
+ borrowed,
565
+ maxBorrow: maxBorrowVal,
566
+ liquidationThreshold: liqThreshold
567
+ };
568
+ }
569
+ async function getRates(client) {
570
+ try {
571
+ const pool = await lending.getPool(USDC_TYPE, ENV);
572
+ let saveApy = rateToApy(pool.currentSupplyRate);
573
+ let borrowApy = rateToApy(pool.currentBorrowRate);
574
+ if (saveApy <= 0 || saveApy > 100) saveApy = 4;
575
+ if (borrowApy <= 0 || borrowApy > 100) borrowApy = 6;
576
+ return { USDC: { saveApy, borrowApy } };
577
+ } catch {
578
+ return { USDC: { saveApy: 4, borrowApy: 6 } };
579
+ }
580
+ }
581
+ async function getPositions(client, keypair) {
582
+ const address = keypair.getPublicKey().toSuiAddress();
583
+ const state = await lending.getLendingState(address, clientOpt(client, true));
584
+ const positions = [];
585
+ for (const pos of state) {
586
+ const symbol = pos.pool.token?.symbol ?? "UNKNOWN";
587
+ const supplyBal = Number(pos.supplyBalance) / 10 ** NAVI_BALANCE_DECIMALS;
588
+ const borrowBal = Number(pos.borrowBalance) / 10 ** NAVI_BALANCE_DECIMALS;
589
+ if (supplyBal > 1e-4) {
590
+ positions.push({
591
+ protocol: "navi",
592
+ asset: symbol,
593
+ type: "save",
594
+ amount: supplyBal,
595
+ apy: rateToApy(pos.pool.currentSupplyRate)
596
+ });
597
+ }
598
+ if (borrowBal > 1e-4) {
599
+ positions.push({
600
+ protocol: "navi",
601
+ asset: symbol,
602
+ type: "borrow",
603
+ amount: borrowBal,
604
+ apy: rateToApy(pos.pool.currentBorrowRate)
605
+ });
606
+ }
607
+ }
608
+ return { positions };
609
+ }
610
+ async function maxWithdrawAmount(client, keypair) {
611
+ const hf = await getHealthFactor(client, keypair);
612
+ const ltv = hf.liquidationThreshold > 0 ? hf.liquidationThreshold : 0.75;
613
+ let maxAmount;
614
+ if (hf.borrowed === 0) {
615
+ maxAmount = hf.supplied;
616
+ } else {
617
+ maxAmount = Math.max(0, hf.supplied - hf.borrowed * MIN_HEALTH_FACTOR / ltv);
618
+ }
619
+ const remainingSupply = hf.supplied - maxAmount;
620
+ const hfAfter = hf.borrowed > 0 ? remainingSupply / hf.borrowed : Infinity;
621
+ return {
622
+ maxAmount,
623
+ healthFactorAfter: hfAfter,
624
+ currentHF: hf.healthFactor
625
+ };
626
+ }
627
+ async function maxBorrowAmount(client, keypair) {
628
+ const hf = await getHealthFactor(client, keypair);
629
+ const ltv = hf.liquidationThreshold > 0 ? hf.liquidationThreshold : 0.75;
630
+ const maxAmount = Math.max(0, hf.supplied * ltv / MIN_HEALTH_FACTOR - hf.borrowed);
631
+ return {
632
+ maxAmount,
633
+ healthFactorAfter: MIN_HEALTH_FACTOR,
634
+ currentHF: hf.healthFactor
635
+ };
636
+ }
637
+ var DEFAULT_SLIPPAGE_BPS = 300;
638
+ function extractGasCost2(effects) {
639
+ if (!effects?.gasUsed) return 0;
640
+ return (Number(effects.gasUsed.computationCost) + Number(effects.gasUsed.storageCost) - Number(effects.gasUsed.storageRebate)) / 1e9;
641
+ }
642
+ function isA2B(from) {
643
+ return from === "USDC";
644
+ }
645
+ var _cetusSDK = null;
646
+ function getCetusSDK() {
647
+ if (!_cetusSDK) {
648
+ _cetusSDK = suiClmmSdk.CetusClmmSDK.createSDK({ env: "mainnet" });
649
+ }
650
+ return _cetusSDK;
651
+ }
652
+ async function executeSwap(params) {
653
+ const { client, keypair, fromAsset, toAsset, amount, maxSlippageBps = DEFAULT_SLIPPAGE_BPS } = params;
654
+ const address = keypair.getPublicKey().toSuiAddress();
655
+ const a2b = isA2B(fromAsset);
656
+ const fromInfo = SUPPORTED_ASSETS[fromAsset];
657
+ const toInfo = SUPPORTED_ASSETS[toAsset];
658
+ const rawAmount = BigInt(Math.floor(amount * 10 ** fromInfo.decimals));
659
+ const sdk = getCetusSDK();
660
+ sdk.setSenderAddress(address);
661
+ const pool = await sdk.Pool.getPool(CETUS_USDC_SUI_POOL);
662
+ const preSwapResult = await sdk.Swap.preSwap({
663
+ pool,
664
+ current_sqrt_price: pool.current_sqrt_price,
665
+ coin_type_a: pool.coin_type_a,
666
+ coin_type_b: pool.coin_type_b,
667
+ decimals_a: 6,
668
+ decimals_b: 9,
669
+ a2b,
670
+ by_amount_in: true,
671
+ amount: rawAmount.toString()
672
+ });
673
+ const estimatedOut = Number(preSwapResult.estimated_amount_out);
674
+ const slippageFactor = (1e4 - maxSlippageBps) / 1e4;
675
+ const amountLimit = Math.floor(estimatedOut * slippageFactor);
676
+ const swapPayload = await sdk.Swap.createSwapPayload({
677
+ pool_id: pool.id,
678
+ coin_type_a: pool.coin_type_a,
679
+ coin_type_b: pool.coin_type_b,
680
+ a2b,
681
+ by_amount_in: true,
682
+ amount: preSwapResult.amount.toString(),
683
+ amount_limit: amountLimit.toString()
684
+ });
685
+ const result = await client.signAndExecuteTransaction({
686
+ signer: keypair,
687
+ transaction: swapPayload,
688
+ options: { showEffects: true, showBalanceChanges: true }
689
+ });
690
+ await client.waitForTransaction({ digest: result.digest });
691
+ let actualReceived = 0;
692
+ if (result.balanceChanges) {
693
+ for (const change of result.balanceChanges) {
694
+ if (change.coinType === toInfo.type && change.owner && typeof change.owner === "object" && "AddressOwner" in change.owner && change.owner.AddressOwner === address) {
695
+ const amt = Number(change.amount) / 10 ** toInfo.decimals;
696
+ if (amt > 0) actualReceived += amt;
697
+ }
698
+ }
699
+ }
700
+ const expectedOutput = estimatedOut / 10 ** toInfo.decimals;
701
+ if (actualReceived === 0) actualReceived = expectedOutput;
702
+ const priceImpact = expectedOutput > 0 ? Math.abs(actualReceived - expectedOutput) / expectedOutput : 0;
703
+ return {
704
+ digest: result.digest,
705
+ fromAmount: amount,
706
+ fromAsset,
707
+ toAmount: actualReceived,
708
+ toAsset,
709
+ priceImpact,
710
+ gasCost: extractGasCost2(result.effects)
711
+ };
712
+ }
713
+ async function getPoolPrice(client) {
714
+ try {
715
+ const pool = await client.getObject({
716
+ id: CETUS_USDC_SUI_POOL,
717
+ options: { showContent: true }
718
+ });
719
+ if (pool.data?.content?.dataType === "moveObject") {
720
+ const fields = pool.data.content.fields;
721
+ const currentSqrtPrice = BigInt(String(fields.current_sqrt_price ?? "0"));
722
+ if (currentSqrtPrice > 0n) {
723
+ const Q64 = 2n ** 64n;
724
+ const sqrtPriceFloat = Number(currentSqrtPrice) / Number(Q64);
725
+ const rawPrice = sqrtPriceFloat * sqrtPriceFloat;
726
+ const suiPriceUsd = 1e3 / rawPrice;
727
+ if (suiPriceUsd > 0.01 && suiPriceUsd < 1e3) return suiPriceUsd;
728
+ }
729
+ }
730
+ } catch {
731
+ }
732
+ return 3.5;
733
+ }
734
+ async function getSwapQuote(client, fromAsset, toAsset, amount) {
735
+ const a2b = isA2B(fromAsset);
736
+ const fromInfo = SUPPORTED_ASSETS[fromAsset];
737
+ const toInfo = SUPPORTED_ASSETS[toAsset];
738
+ const rawAmount = BigInt(Math.floor(amount * 10 ** fromInfo.decimals));
739
+ const poolPrice = await getPoolPrice(client);
740
+ try {
741
+ const sdk = getCetusSDK();
742
+ const pool = await sdk.Pool.getPool(CETUS_USDC_SUI_POOL);
743
+ const preSwapResult = await sdk.Swap.preSwap({
744
+ pool,
745
+ current_sqrt_price: pool.current_sqrt_price,
746
+ coin_type_a: pool.coin_type_a,
747
+ coin_type_b: pool.coin_type_b,
748
+ decimals_a: 6,
749
+ decimals_b: 9,
750
+ a2b,
751
+ by_amount_in: true,
752
+ amount: rawAmount.toString()
753
+ });
754
+ const expectedOutput = Number(preSwapResult.estimated_amount_out) / 10 ** toInfo.decimals;
755
+ return { expectedOutput, priceImpact: 0, poolPrice };
756
+ } catch {
757
+ let expectedOutput;
758
+ if (fromAsset === "USDC") {
759
+ expectedOutput = amount / poolPrice;
760
+ } else {
761
+ expectedOutput = amount * poolPrice;
762
+ }
763
+ return { expectedOutput, priceImpact: 0, poolPrice };
764
+ }
765
+ }
766
+
767
+ // src/protocols/protocolFee.ts
768
+ var FEE_RATES = {
769
+ save: SAVE_FEE_BPS,
770
+ swap: SWAP_FEE_BPS,
771
+ borrow: BORROW_FEE_BPS
772
+ };
773
+ function calculateFee(operation, amount) {
774
+ const bps = FEE_RATES[operation];
775
+ const feeAmount = amount * Number(bps) / Number(BPS_DENOMINATOR);
776
+ const rawAmount = usdcToRaw(feeAmount);
777
+ return {
778
+ amount: feeAmount,
779
+ asset: "USDC",
780
+ rate: Number(bps) / Number(BPS_DENOMINATOR),
781
+ rawAmount
782
+ };
783
+ }
784
+ async function reportFee(agentAddress, operation, feeAmount, feeRate, txDigest) {
785
+ try {
786
+ await fetch(`${API_BASE_URL}/api/fees`, {
787
+ method: "POST",
788
+ headers: { "Content-Type": "application/json" },
789
+ body: JSON.stringify({
790
+ agentAddress,
791
+ operation,
792
+ feeAmount: feeAmount.toString(),
793
+ feeRate: feeRate.toString(),
794
+ txDigest
795
+ })
796
+ });
797
+ } catch {
798
+ }
799
+ }
800
+
801
+ // src/protocols/yieldTracker.ts
802
+ async function getEarnings(client, keypair) {
803
+ const hf = await getHealthFactor(client, keypair);
804
+ const rates = await getRates();
805
+ const supplied = hf.supplied;
806
+ const apy = rates.USDC.saveApy / 100;
807
+ const dailyRate = apy / 365;
808
+ const dailyEarning = supplied * dailyRate;
809
+ const totalYieldEarned = dailyEarning * 30;
810
+ return {
811
+ totalYieldEarned,
812
+ currentApy: rates.USDC.saveApy,
813
+ dailyEarning,
814
+ supplied
815
+ };
816
+ }
817
+ async function getFundStatus(client, keypair) {
818
+ const earnings = await getEarnings(client, keypair);
819
+ return {
820
+ supplied: earnings.supplied,
821
+ apy: earnings.currentApy,
822
+ earnedToday: earnings.dailyEarning,
823
+ earnedAllTime: earnings.totalYieldEarned,
824
+ projectedMonthly: earnings.dailyEarning * 30
825
+ };
826
+ }
827
+ function hasLeadingZeroBits(hash, bits) {
828
+ const fullBytes = Math.floor(bits / 8);
829
+ const remainingBits = bits % 8;
830
+ for (let i = 0; i < fullBytes; i++) {
831
+ if (hash[i] !== 0) return false;
832
+ }
833
+ if (remainingBits > 0) {
834
+ const mask = 255 << 8 - remainingBits;
835
+ if ((hash[fullBytes] & mask) !== 0) return false;
836
+ }
837
+ return true;
838
+ }
839
+ function solveHashcash(challenge) {
840
+ const bits = parseInt(challenge.split(":")[1], 10);
841
+ let counter = 0;
842
+ while (true) {
843
+ const stamp = `${challenge}${counter.toString(16)}`;
844
+ const hash = crypto.createHash("sha256").update(stamp).digest();
845
+ if (hasLeadingZeroBits(hash, bits)) return stamp;
846
+ counter++;
847
+ }
848
+ }
849
+
850
+ // src/gas/gasStation.ts
851
+ async function requestGasSponsorship(txBytesBase64, sender, type) {
852
+ const res = await fetch(`${API_BASE_URL}/api/gas`, {
853
+ method: "POST",
854
+ headers: { "Content-Type": "application/json" },
855
+ body: JSON.stringify({ txBytes: txBytesBase64, sender, type })
856
+ });
857
+ const data = await res.json();
858
+ if (!res.ok) {
859
+ const errorCode = data.error;
860
+ if (errorCode === "CIRCUIT_BREAKER" || errorCode === "POOL_DEPLETED") {
861
+ throw new T2000Error(
862
+ "GAS_STATION_UNAVAILABLE",
863
+ data.message ?? "Gas station temporarily unavailable",
864
+ { retryAfter: data.retryAfter },
865
+ true
866
+ );
867
+ }
868
+ if (errorCode === "GAS_FEE_EXCEEDED") {
869
+ throw new T2000Error(
870
+ "GAS_FEE_EXCEEDED",
871
+ data.message ?? "Gas fee exceeds ceiling",
872
+ { retryAfter: data.retryAfter },
873
+ true
874
+ );
875
+ }
876
+ throw new T2000Error(
877
+ "GAS_STATION_UNAVAILABLE",
878
+ data.message ?? "Gas sponsorship request failed",
879
+ void 0,
880
+ true
881
+ );
882
+ }
883
+ return data;
884
+ }
885
+ async function reportGasUsage(sender, txDigest, gasCostSui, usdcCharged, type) {
886
+ try {
887
+ await fetch(`${API_BASE_URL}/api/gas/report`, {
888
+ method: "POST",
889
+ headers: { "Content-Type": "application/json" },
890
+ body: JSON.stringify({ sender, txDigest, gasCostSui, usdcCharged, type })
891
+ });
892
+ } catch {
893
+ }
894
+ }
895
+ async function getGasStatus(address) {
896
+ const url = new URL(`${API_BASE_URL}/api/gas/status`);
897
+ if (address) url.searchParams.set("address", address);
898
+ const res = await fetch(url.toString());
899
+ if (!res.ok) {
900
+ throw new T2000Error("GAS_STATION_UNAVAILABLE", "Failed to fetch gas status", void 0, true);
901
+ }
902
+ return await res.json();
903
+ }
904
+
905
+ // src/gas/autoTopUp.ts
906
+ async function shouldAutoTopUp(client, address) {
907
+ const [suiBalance, usdcBalance] = await Promise.all([
908
+ client.getBalance({ owner: address, coinType: SUPPORTED_ASSETS.SUI.type }),
909
+ client.getBalance({ owner: address, coinType: SUPPORTED_ASSETS.USDC.type })
910
+ ]);
911
+ const suiRaw = BigInt(suiBalance.totalBalance);
912
+ const usdcRaw = BigInt(usdcBalance.totalBalance);
913
+ return suiRaw < AUTO_TOPUP_THRESHOLD && usdcRaw >= AUTO_TOPUP_MIN_USDC;
914
+ }
915
+ async function executeAutoTopUp(client, keypair) {
916
+ const address = keypair.getPublicKey().toSuiAddress();
917
+ const tx = new transactions.Transaction();
918
+ tx.setSender(address);
919
+ const usdcCoins = await client.getCoins({
920
+ owner: address,
921
+ coinType: SUPPORTED_ASSETS.USDC.type
922
+ });
923
+ if (usdcCoins.data.length === 0) {
924
+ throw new T2000Error("AUTO_TOPUP_FAILED", "No USDC coins available for auto-topup");
925
+ }
926
+ const coinIds = usdcCoins.data.map((c) => c.coinObjectId);
927
+ let usdcCoin;
928
+ if (coinIds.length === 1) {
929
+ usdcCoin = tx.splitCoins(tx.object(coinIds[0]), [AUTO_TOPUP_AMOUNT]);
930
+ } else {
931
+ const primary = tx.object(coinIds[0]);
932
+ if (coinIds.length > 1) {
933
+ tx.mergeCoins(primary, coinIds.slice(1).map((id) => tx.object(id)));
934
+ }
935
+ usdcCoin = tx.splitCoins(primary, [AUTO_TOPUP_AMOUNT]);
936
+ }
937
+ const MIN_SQRT_PRICE = "4295048016";
938
+ const [receivedCoin, returnedCoin] = tx.moveCall({
939
+ target: `${CETUS_PACKAGE}::pool_script::swap_a2b`,
940
+ arguments: [
941
+ tx.object(CETUS_GLOBAL_CONFIG),
942
+ tx.object(CETUS_USDC_SUI_POOL),
943
+ usdcCoin,
944
+ tx.pure.bool(true),
945
+ // by_amount_in
946
+ tx.pure.u64(AUTO_TOPUP_AMOUNT),
947
+ tx.pure.u128(MIN_SQRT_PRICE),
948
+ tx.object(CLOCK_ID)
949
+ ],
950
+ typeArguments: [SUPPORTED_ASSETS.USDC.type, SUPPORTED_ASSETS.SUI.type]
951
+ });
952
+ tx.transferObjects([receivedCoin], address);
953
+ tx.transferObjects([returnedCoin], address);
954
+ const txBytes = await tx.build({ client, onlyTransactionKind: true });
955
+ const txBytesBase64 = Buffer.from(txBytes).toString("base64");
956
+ let sponsoredResult;
957
+ try {
958
+ sponsoredResult = await requestGasSponsorship(txBytesBase64, address, "auto-topup");
959
+ } catch {
960
+ throw new T2000Error("AUTO_TOPUP_FAILED", "Gas station unavailable for auto-topup sponsorship");
961
+ }
962
+ const sponsoredTxBytes = Buffer.from(sponsoredResult.txBytes, "base64");
963
+ const { signature: agentSig } = await keypair.signTransaction(sponsoredTxBytes);
964
+ const result = await client.executeTransactionBlock({
965
+ transactionBlock: sponsoredResult.txBytes,
966
+ signature: [agentSig, sponsoredResult.sponsorSignature],
967
+ options: { showEffects: true, showBalanceChanges: true }
968
+ });
969
+ await client.waitForTransaction({ digest: result.digest });
970
+ let suiReceived = 0;
971
+ if (result.balanceChanges) {
972
+ for (const change of result.balanceChanges) {
973
+ if (change.coinType === SUPPORTED_ASSETS.SUI.type && change.owner && typeof change.owner === "object" && "AddressOwner" in change.owner && change.owner.AddressOwner === address) {
974
+ suiReceived += Number(change.amount) / Number(MIST_PER_SUI);
975
+ }
976
+ }
977
+ }
978
+ reportGasUsage(address, result.digest, 0, 0, "auto-topup");
979
+ return {
980
+ success: true,
981
+ tx: result.digest,
982
+ usdcSpent: Number(AUTO_TOPUP_AMOUNT) / 1e6,
983
+ suiReceived: Math.abs(suiReceived)
984
+ };
985
+ }
986
+
987
+ // src/t2000.ts
988
+ var T2000 = class _T2000 extends eventemitter3.EventEmitter {
989
+ keypair;
990
+ client;
991
+ _address;
992
+ _lastGasMethod = "self-funded";
993
+ constructor(keypair, client) {
994
+ super();
995
+ this.keypair = keypair;
996
+ this.client = client;
997
+ this._address = getAddress(keypair);
998
+ }
999
+ static async create(options = {}) {
1000
+ const { keyPath, passphrase, network = DEFAULT_NETWORK, rpcUrl, sponsored, name } = options;
1001
+ const client = getSuiClient(rpcUrl);
1002
+ if (sponsored) {
1003
+ const keypair2 = generateKeypair();
1004
+ if (passphrase) {
1005
+ await saveKey(keypair2, passphrase, keyPath);
1006
+ }
1007
+ return new _T2000(keypair2, client);
1008
+ }
1009
+ const exists = await walletExists(keyPath);
1010
+ if (!exists) {
1011
+ throw new T2000Error(
1012
+ "WALLET_NOT_FOUND",
1013
+ "No wallet found. Run `t2000 init` to create one."
1014
+ );
1015
+ }
1016
+ if (!passphrase) {
1017
+ throw new T2000Error("WALLET_LOCKED", "Passphrase required to unlock wallet");
1018
+ }
1019
+ const keypair = await loadKey(passphrase, keyPath);
1020
+ return new _T2000(keypair, client);
1021
+ }
1022
+ static fromPrivateKey(privateKey, options = {}) {
1023
+ const keypair = keypairFromPrivateKey(privateKey);
1024
+ const client = getSuiClient(options.rpcUrl);
1025
+ return new _T2000(keypair, client);
1026
+ }
1027
+ static async init(options) {
1028
+ const keypair = generateKeypair();
1029
+ await saveKey(keypair, options.passphrase, options.keyPath);
1030
+ const client = getSuiClient();
1031
+ const agent = new _T2000(keypair, client);
1032
+ const address = agent.address();
1033
+ let sponsored = false;
1034
+ if (options.sponsored !== false) {
1035
+ try {
1036
+ await callSponsorApi(address, options.name);
1037
+ sponsored = true;
1038
+ } catch {
1039
+ }
1040
+ }
1041
+ return { agent, address, sponsored };
1042
+ }
1043
+ // -- Gas --
1044
+ /**
1045
+ * Ensure the agent has enough SUI for gas.
1046
+ * If SUI is low and USDC is available, auto-swaps $1 USDC → SUI.
1047
+ */
1048
+ async ensureGas() {
1049
+ this._lastGasMethod = "self-funded";
1050
+ const needsTopUp = await shouldAutoTopUp(this.client, this._address);
1051
+ if (!needsTopUp) return;
1052
+ try {
1053
+ const result = await executeAutoTopUp(this.client, this.keypair);
1054
+ this._lastGasMethod = "auto-topup";
1055
+ this.emit("gasAutoTopUp", {
1056
+ usdcSpent: result.usdcSpent,
1057
+ suiReceived: result.suiReceived
1058
+ });
1059
+ } catch {
1060
+ this.emit("gasStationFallback", {
1061
+ reason: "auto-topup failed",
1062
+ method: "self-funded",
1063
+ suiUsed: 0
1064
+ });
1065
+ }
1066
+ }
1067
+ /** SuiClient used by this agent — exposed for x402 and other integrations. */
1068
+ get suiClient() {
1069
+ return this.client;
1070
+ }
1071
+ /** Ed25519Keypair used by this agent — exposed for x402 and other integrations. */
1072
+ get signer() {
1073
+ return this.keypair;
1074
+ }
1075
+ // -- Wallet --
1076
+ address() {
1077
+ return this._address;
1078
+ }
1079
+ async send(params) {
1080
+ const asset = params.asset ?? "USDC";
1081
+ if (!(asset in SUPPORTED_ASSETS)) {
1082
+ throw new T2000Error("ASSET_NOT_SUPPORTED", `Asset ${asset} is not supported`);
1083
+ }
1084
+ await this.ensureGas();
1085
+ const result = await buildAndExecuteSend({
1086
+ client: this.client,
1087
+ keypair: this.keypair,
1088
+ to: params.to,
1089
+ amount: params.amount,
1090
+ asset
1091
+ });
1092
+ const balance = await this.balance();
1093
+ this.emitBalanceChange(asset, params.amount, "send", result.digest);
1094
+ return {
1095
+ success: true,
1096
+ tx: result.digest,
1097
+ amount: params.amount,
1098
+ to: params.to,
1099
+ gasCost: result.gasCost,
1100
+ gasCostUnit: "SUI",
1101
+ gasMethod: this._lastGasMethod,
1102
+ balance
1103
+ };
1104
+ }
1105
+ async balance() {
1106
+ const bal = await queryBalance(this.client, this._address);
1107
+ try {
1108
+ const positions = await this.positions();
1109
+ const savings = positions.positions.filter((p) => p.type === "save").reduce((sum, p) => sum + p.amount, 0);
1110
+ bal.savings = savings;
1111
+ bal.total = bal.available + savings + bal.gasReserve.usdEquiv;
1112
+ } catch {
1113
+ }
1114
+ return bal;
1115
+ }
1116
+ async history(params) {
1117
+ return queryHistory(this.client, this._address, params?.limit);
1118
+ }
1119
+ async deposit() {
1120
+ return {
1121
+ address: this._address,
1122
+ network: "Sui (mainnet)",
1123
+ supportedAssets: ["USDC"],
1124
+ instructions: [
1125
+ `Send USDC on Sui to: ${this._address}`,
1126
+ "",
1127
+ "From a CEX (Coinbase, Binance):",
1128
+ ` 1. Withdraw USDC`,
1129
+ ` 2. Select "Sui" network`,
1130
+ ` 3. Paste address: ${truncateAddress(this._address)}`,
1131
+ "",
1132
+ "From another Sui wallet:",
1133
+ ` Transfer USDC to ${truncateAddress(this._address)}`
1134
+ ].join("\n")
1135
+ };
1136
+ }
1137
+ exportKey() {
1138
+ return exportPrivateKey(this.keypair);
1139
+ }
1140
+ // -- Savings --
1141
+ async save(params) {
1142
+ let amount;
1143
+ if (params.amount === "all") {
1144
+ const bal = await queryBalance(this.client, this._address);
1145
+ const GAS_RESERVE_USDC = 1;
1146
+ amount = bal.available - GAS_RESERVE_USDC;
1147
+ if (amount <= 0) {
1148
+ throw new T2000Error("INSUFFICIENT_BALANCE", "Balance too low to save after $1 gas reserve", {
1149
+ reason: "gas_reserve_required",
1150
+ available: bal.available
1151
+ });
1152
+ }
1153
+ } else {
1154
+ amount = params.amount;
1155
+ }
1156
+ const fee = calculateFee("save", amount);
1157
+ await this.ensureGas();
1158
+ const result = await save(this.client, this.keypair, amount);
1159
+ reportFee(this._address, "save", fee.amount, fee.rate, result.tx);
1160
+ this.emitBalanceChange("USDC", amount, "save", result.tx);
1161
+ return { ...result, fee: fee.amount, gasMethod: this._lastGasMethod };
1162
+ }
1163
+ async withdraw(params) {
1164
+ let amount;
1165
+ if (params.amount === "all") {
1166
+ const maxResult = await this.maxWithdraw();
1167
+ amount = maxResult.maxAmount;
1168
+ if (amount <= 0) {
1169
+ throw new T2000Error("NO_COLLATERAL", "No savings to withdraw");
1170
+ }
1171
+ } else {
1172
+ amount = params.amount;
1173
+ const hf = await this.healthFactor();
1174
+ if (hf.borrowed > 0) {
1175
+ const maxResult = await this.maxWithdraw();
1176
+ if (amount > maxResult.maxAmount) {
1177
+ throw new T2000Error(
1178
+ "WITHDRAW_WOULD_LIQUIDATE",
1179
+ `Withdrawing $${amount.toFixed(2)} would drop health factor below 1.5`,
1180
+ {
1181
+ safeWithdrawAmount: maxResult.maxAmount,
1182
+ currentHF: maxResult.currentHF,
1183
+ projectedHF: maxResult.healthFactorAfter
1184
+ }
1185
+ );
1186
+ }
1187
+ }
1188
+ }
1189
+ await this.ensureGas();
1190
+ const result = await withdraw(this.client, this.keypair, amount);
1191
+ this.emitBalanceChange("USDC", amount, "withdraw", result.tx);
1192
+ return { ...result, gasMethod: this._lastGasMethod };
1193
+ }
1194
+ async maxWithdraw() {
1195
+ return maxWithdrawAmount(this.client, this.keypair);
1196
+ }
1197
+ // -- Borrowing --
1198
+ async borrow(params) {
1199
+ const maxResult = await this.maxBorrow();
1200
+ if (params.amount > maxResult.maxAmount) {
1201
+ throw new T2000Error("HEALTH_FACTOR_TOO_LOW", `Max safe borrow: $${maxResult.maxAmount.toFixed(2)}`, {
1202
+ maxBorrow: maxResult.maxAmount,
1203
+ currentHF: maxResult.currentHF
1204
+ });
1205
+ }
1206
+ const fee = calculateFee("borrow", params.amount);
1207
+ await this.ensureGas();
1208
+ const result = await borrow(this.client, this.keypair, params.amount);
1209
+ reportFee(this._address, "borrow", fee.amount, fee.rate, result.tx);
1210
+ this.emitBalanceChange("USDC", params.amount, "borrow", result.tx);
1211
+ return { ...result, fee: fee.amount, gasMethod: this._lastGasMethod };
1212
+ }
1213
+ async repay(params) {
1214
+ let amount;
1215
+ if (params.amount === "all") {
1216
+ const hf = await this.healthFactor();
1217
+ amount = hf.borrowed;
1218
+ if (amount <= 0) {
1219
+ throw new T2000Error("NO_COLLATERAL", "No outstanding borrow to repay");
1220
+ }
1221
+ } else {
1222
+ amount = params.amount;
1223
+ }
1224
+ await this.ensureGas();
1225
+ const result = await repay(this.client, this.keypair, amount);
1226
+ this.emitBalanceChange("USDC", amount, "repay", result.tx);
1227
+ return { ...result, gasMethod: this._lastGasMethod };
1228
+ }
1229
+ async maxBorrow() {
1230
+ return maxBorrowAmount(this.client, this.keypair);
1231
+ }
1232
+ async healthFactor() {
1233
+ const hf = await getHealthFactor(this.client, this.keypair);
1234
+ if (hf.healthFactor < 1.2) {
1235
+ this.emit("healthCritical", { healthFactor: hf.healthFactor, threshold: 1.5, severity: "critical" });
1236
+ } else if (hf.healthFactor < 2) {
1237
+ this.emit("healthWarning", { healthFactor: hf.healthFactor, threshold: 2, severity: "warning" });
1238
+ }
1239
+ return hf;
1240
+ }
1241
+ // -- Swap --
1242
+ async swap(params) {
1243
+ const fromAsset = params.from.toUpperCase();
1244
+ const toAsset = params.to.toUpperCase();
1245
+ if (!(fromAsset in SUPPORTED_ASSETS) || !(toAsset in SUPPORTED_ASSETS)) {
1246
+ throw new T2000Error("ASSET_NOT_SUPPORTED", `Swap pair ${fromAsset}/${toAsset} is not supported`);
1247
+ }
1248
+ if (fromAsset === toAsset) {
1249
+ throw new T2000Error("INVALID_AMOUNT", "Cannot swap same asset");
1250
+ }
1251
+ const fee = calculateFee("swap", params.amount);
1252
+ await this.ensureGas();
1253
+ const result = await executeSwap({
1254
+ client: this.client,
1255
+ keypair: this.keypair,
1256
+ fromAsset,
1257
+ toAsset,
1258
+ amount: params.amount,
1259
+ maxSlippageBps: params.maxSlippage ? params.maxSlippage * 100 : void 0
1260
+ });
1261
+ reportFee(this._address, "swap", fee.amount, fee.rate, result.digest);
1262
+ this.emitBalanceChange(result.fromAsset, result.fromAmount, "swap", result.digest);
1263
+ return {
1264
+ success: true,
1265
+ tx: result.digest,
1266
+ fromAmount: result.fromAmount,
1267
+ fromAsset: result.fromAsset,
1268
+ toAmount: result.toAmount,
1269
+ toAsset: result.toAsset,
1270
+ priceImpact: result.priceImpact,
1271
+ fee: fee.amount,
1272
+ gasCost: result.gasCost,
1273
+ gasMethod: this._lastGasMethod
1274
+ };
1275
+ }
1276
+ async swapQuote(params) {
1277
+ const fromAsset = params.from.toUpperCase();
1278
+ const toAsset = params.to.toUpperCase();
1279
+ const quote = await getSwapQuote(this.client, fromAsset, toAsset, params.amount);
1280
+ const fee = calculateFee("swap", params.amount);
1281
+ return { ...quote, fee: { amount: fee.amount, rate: fee.rate } };
1282
+ }
1283
+ // -- Info --
1284
+ async positions() {
1285
+ return getPositions(this.client, this.keypair);
1286
+ }
1287
+ async rates() {
1288
+ return getRates(this.client);
1289
+ }
1290
+ async earnings() {
1291
+ const result = await getEarnings(this.client, this.keypair);
1292
+ if (result.totalYieldEarned > 0) {
1293
+ this.emit("yield", {
1294
+ earned: result.dailyEarning,
1295
+ total: result.totalYieldEarned,
1296
+ apy: result.currentApy / 100,
1297
+ timestamp: Date.now()
1298
+ });
1299
+ }
1300
+ return result;
1301
+ }
1302
+ async fundStatus() {
1303
+ return getFundStatus(this.client, this.keypair);
1304
+ }
1305
+ // -- Helpers --
1306
+ emitBalanceChange(asset, amount, cause, tx) {
1307
+ this.emit("balanceChange", { asset, previous: 0, current: 0, cause, tx });
1308
+ }
1309
+ };
1310
+ async function callSponsorApi(address, name) {
1311
+ const res = await fetch(`${API_BASE_URL}/api/sponsor`, {
1312
+ method: "POST",
1313
+ headers: { "Content-Type": "application/json" },
1314
+ body: JSON.stringify({ address, name })
1315
+ });
1316
+ if (res.status === 429) {
1317
+ const data = await res.json();
1318
+ if (data.challenge) {
1319
+ const proof = solveHashcash(data.challenge);
1320
+ const retry = await fetch(`${API_BASE_URL}/api/sponsor`, {
1321
+ method: "POST",
1322
+ headers: { "Content-Type": "application/json" },
1323
+ body: JSON.stringify({ address, name, proof })
1324
+ });
1325
+ if (!retry.ok) throw new T2000Error("SPONSOR_RATE_LIMITED", "Sponsor rate limited");
1326
+ return;
1327
+ }
1328
+ }
1329
+ if (!res.ok) {
1330
+ throw new T2000Error("SPONSOR_FAILED", "Sponsor API unavailable");
1331
+ }
1332
+ }
1333
+
1334
+ // src/utils/simulate.ts
1335
+ async function simulateTransaction(client, tx, sender) {
1336
+ tx.setSender(sender);
1337
+ try {
1338
+ const txBytes = await tx.build({ client });
1339
+ const dryRun = await client.dryRunTransactionBlock({
1340
+ transactionBlock: Buffer.from(txBytes).toString("base64")
1341
+ });
1342
+ const status = dryRun.effects?.status;
1343
+ const gasUsed = dryRun.effects?.gasUsed;
1344
+ const gasEstimateSui = gasUsed ? (Number(gasUsed.computationCost) + Number(gasUsed.storageCost) - Number(gasUsed.storageRebate)) / 1e9 : 0;
1345
+ if (status?.status === "failure") {
1346
+ const rawError = status.error ?? "Unknown simulation error";
1347
+ const parsed = parseMoveAbort(rawError);
1348
+ return {
1349
+ success: false,
1350
+ gasEstimateSui,
1351
+ error: {
1352
+ moveAbortCode: parsed.abortCode,
1353
+ moveModule: parsed.module,
1354
+ reason: parsed.reason,
1355
+ rawError
1356
+ }
1357
+ };
1358
+ }
1359
+ return { success: true, gasEstimateSui };
1360
+ } catch (err) {
1361
+ const rawError = err instanceof Error ? err.message : String(err);
1362
+ return {
1363
+ success: false,
1364
+ gasEstimateSui: 0,
1365
+ error: {
1366
+ reason: "Simulation failed: " + rawError,
1367
+ rawError
1368
+ }
1369
+ };
1370
+ }
1371
+ }
1372
+ function throwIfSimulationFailed(sim) {
1373
+ if (sim.success) return;
1374
+ throw new T2000Error(
1375
+ "SIMULATION_FAILED",
1376
+ sim.error?.reason ?? "Transaction simulation failed",
1377
+ {
1378
+ moveAbortCode: sim.error?.moveAbortCode,
1379
+ moveModule: sim.error?.moveModule,
1380
+ reason: sim.error?.reason,
1381
+ rawError: sim.error?.rawError
1382
+ }
1383
+ );
1384
+ }
1385
+ function parseMoveAbort(errorStr) {
1386
+ const abortMatch = errorStr.match(/MoveAbort\([^,]*,\s*(\d+)\)/);
1387
+ const moduleMatch = errorStr.match(/name:\s*Identifier\("([^"]+)"\)/);
1388
+ if (abortMatch) {
1389
+ const code = parseInt(abortMatch[1], 10);
1390
+ const module = moduleMatch?.[1];
1391
+ const reason = mapMoveAbortCode(code);
1392
+ return { abortCode: code, module, reason };
1393
+ }
1394
+ if (errorStr.includes("MovePrimitiveRuntimeError")) {
1395
+ const module = moduleMatch?.[1];
1396
+ return {
1397
+ module,
1398
+ reason: `Move runtime error in ${module ?? "unknown"} module`
1399
+ };
1400
+ }
1401
+ return { reason: errorStr };
1402
+ }
1403
+
1404
+ // src/gas/manager.ts
1405
+ function extractGasCost3(effects) {
1406
+ if (!effects?.gasUsed) return 0;
1407
+ return (Number(effects.gasUsed.computationCost) + Number(effects.gasUsed.storageCost) - Number(effects.gasUsed.storageRebate)) / 1e9;
1408
+ }
1409
+ async function getSuiBalance(client, address) {
1410
+ const bal = await client.getBalance({ owner: address, coinType: SUPPORTED_ASSETS.SUI.type });
1411
+ return BigInt(bal.totalBalance);
1412
+ }
1413
+ async function trySelfFunded(client, keypair, tx) {
1414
+ const address = keypair.getPublicKey().toSuiAddress();
1415
+ const suiBalance = await getSuiBalance(client, address);
1416
+ if (suiBalance < AUTO_TOPUP_THRESHOLD) return null;
1417
+ tx.setSender(address);
1418
+ const result = await client.signAndExecuteTransaction({
1419
+ signer: keypair,
1420
+ transaction: tx,
1421
+ options: { showEffects: true }
1422
+ });
1423
+ await client.waitForTransaction({ digest: result.digest });
1424
+ return {
1425
+ digest: result.digest,
1426
+ effects: result.effects,
1427
+ gasMethod: "self-funded",
1428
+ gasCostSui: extractGasCost3(result.effects)
1429
+ };
1430
+ }
1431
+ async function tryAutoTopUpThenSelfFund(client, keypair, tx) {
1432
+ const address = keypair.getPublicKey().toSuiAddress();
1433
+ const canTopUp = await shouldAutoTopUp(client, address);
1434
+ if (!canTopUp) return null;
1435
+ try {
1436
+ await executeAutoTopUp(client, keypair);
1437
+ } catch {
1438
+ return null;
1439
+ }
1440
+ tx.setSender(address);
1441
+ const result = await client.signAndExecuteTransaction({
1442
+ signer: keypair,
1443
+ transaction: tx,
1444
+ options: { showEffects: true }
1445
+ });
1446
+ await client.waitForTransaction({ digest: result.digest });
1447
+ return {
1448
+ digest: result.digest,
1449
+ effects: result.effects,
1450
+ gasMethod: "auto-topup",
1451
+ gasCostSui: extractGasCost3(result.effects)
1452
+ };
1453
+ }
1454
+ async function trySponsored(client, keypair, tx) {
1455
+ const address = keypair.getPublicKey().toSuiAddress();
1456
+ tx.setSender(address);
1457
+ let txBytes;
1458
+ try {
1459
+ txBytes = await tx.build({ client, onlyTransactionKind: true });
1460
+ } catch {
1461
+ return null;
1462
+ }
1463
+ const txBytesBase64 = Buffer.from(txBytes).toString("base64");
1464
+ let sponsoredResult;
1465
+ try {
1466
+ sponsoredResult = await requestGasSponsorship(txBytesBase64, address);
1467
+ } catch {
1468
+ return null;
1469
+ }
1470
+ const sponsoredTxBytes = Buffer.from(sponsoredResult.txBytes, "base64");
1471
+ const { signature: agentSig } = await keypair.signTransaction(sponsoredTxBytes);
1472
+ const result = await client.executeTransactionBlock({
1473
+ transactionBlock: sponsoredResult.txBytes,
1474
+ signature: [agentSig, sponsoredResult.sponsorSignature],
1475
+ options: { showEffects: true }
1476
+ });
1477
+ await client.waitForTransaction({ digest: result.digest });
1478
+ const gasCost = extractGasCost3(result.effects);
1479
+ reportGasUsage(address, result.digest, gasCost, 0, sponsoredResult.type);
1480
+ return {
1481
+ digest: result.digest,
1482
+ effects: result.effects,
1483
+ gasMethod: "sponsored",
1484
+ gasCostSui: gasCost
1485
+ };
1486
+ }
1487
+ async function executeWithGas(client, keypair, buildTx) {
1488
+ try {
1489
+ const tx = await buildTx();
1490
+ const result = await trySelfFunded(client, keypair, tx);
1491
+ if (result) return result;
1492
+ } catch {
1493
+ }
1494
+ try {
1495
+ const tx = await buildTx();
1496
+ const result = await tryAutoTopUpThenSelfFund(client, keypair, tx);
1497
+ if (result) return result;
1498
+ } catch {
1499
+ }
1500
+ try {
1501
+ const tx = await buildTx();
1502
+ const result = await trySponsored(client, keypair, tx);
1503
+ if (result) return result;
1504
+ } catch {
1505
+ }
1506
+ throw new T2000Error(
1507
+ "INSUFFICIENT_GAS",
1508
+ "No SUI for gas and Gas Station unavailable. Fund your wallet with SUI or USDC.",
1509
+ { reason: "all_gas_methods_exhausted" }
1510
+ );
1511
+ }
1512
+
1513
+ exports.BPS_DENOMINATOR = BPS_DENOMINATOR;
1514
+ exports.CLOCK_ID = CLOCK_ID;
1515
+ exports.DEFAULT_NETWORK = DEFAULT_NETWORK;
1516
+ exports.MIST_PER_SUI = MIST_PER_SUI;
1517
+ exports.SUI_DECIMALS = SUI_DECIMALS;
1518
+ exports.SUPPORTED_ASSETS = SUPPORTED_ASSETS;
1519
+ exports.T2000 = T2000;
1520
+ exports.T2000Error = T2000Error;
1521
+ exports.USDC_DECIMALS = USDC_DECIMALS;
1522
+ exports.calculateFee = calculateFee;
1523
+ exports.executeAutoTopUp = executeAutoTopUp;
1524
+ exports.executeWithGas = executeWithGas;
1525
+ exports.exportPrivateKey = exportPrivateKey;
1526
+ exports.formatSui = formatSui;
1527
+ exports.formatUsd = formatUsd;
1528
+ exports.generateKeypair = generateKeypair;
1529
+ exports.getAddress = getAddress;
1530
+ exports.getGasStatus = getGasStatus;
1531
+ exports.getPoolPrice = getPoolPrice;
1532
+ exports.getRates = getRates;
1533
+ exports.getSwapQuote = getSwapQuote;
1534
+ exports.keypairFromPrivateKey = keypairFromPrivateKey;
1535
+ exports.loadKey = loadKey;
1536
+ exports.mapMoveAbortCode = mapMoveAbortCode;
1537
+ exports.mapWalletError = mapWalletError;
1538
+ exports.mistToSui = mistToSui;
1539
+ exports.rawToUsdc = rawToUsdc;
1540
+ exports.saveKey = saveKey;
1541
+ exports.shouldAutoTopUp = shouldAutoTopUp;
1542
+ exports.simulateTransaction = simulateTransaction;
1543
+ exports.solveHashcash = solveHashcash;
1544
+ exports.suiToMist = suiToMist;
1545
+ exports.throwIfSimulationFailed = throwIfSimulationFailed;
1546
+ exports.truncateAddress = truncateAddress;
1547
+ exports.usdcToRaw = usdcToRaw;
1548
+ exports.validateAddress = validateAddress;
1549
+ exports.walletExists = walletExists;
1550
+ //# sourceMappingURL=index.cjs.map
1551
+ //# sourceMappingURL=index.cjs.map