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