@t2000/sdk 3.3.0 → 4.0.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 CHANGED
@@ -6,10 +6,10 @@ var BN = require('bn.js');
6
6
  var eventemitter3 = require('eventemitter3');
7
7
  var paymentKit = require('@mysten/payment-kit');
8
8
  var jsonRpc = require('@mysten/sui/jsonRpc');
9
+ var grpc = require('@mysten/sui/grpc');
9
10
  var utils = require('@mysten/sui/utils');
10
11
  var ed25519 = require('@mysten/sui/keypairs/ed25519');
11
12
  var cryptography = require('@mysten/sui/cryptography');
12
- var crypto$1 = require('crypto');
13
13
  var promises = require('fs/promises');
14
14
  var path = require('path');
15
15
  var os = require('os');
@@ -1128,7 +1128,7 @@ var OPERATION_ASSETS = {
1128
1128
  borrow: ["USDC", "USDsui"],
1129
1129
  withdraw: "*",
1130
1130
  repay: "*",
1131
- send: "*",
1131
+ send: ["USDC", "USDsui", "SUI"],
1132
1132
  swap: "*"
1133
1133
  };
1134
1134
  function isAllowedAsset(op, asset) {
@@ -1142,28 +1142,62 @@ function assertAllowedAsset(op, asset) {
1142
1142
  if (!isAllowedAsset(op, asset)) {
1143
1143
  const allowed = OPERATION_ASSETS[op];
1144
1144
  const list = Array.isArray(allowed) ? allowed.join(", ") : "any";
1145
+ const swapHint = op === "save" ? " Swap to USDC or USDsui first." : op === "send" ? " Swap to USDC or USDsui first, or send SUI." : "";
1145
1146
  throw new exports.T2000Error(
1146
1147
  "INVALID_ASSET",
1147
- `${op} only supports ${list}. Cannot use ${asset}.${op === "save" ? " Swap to USDC or USDsui first." : ""}`
1148
+ `${op} only supports ${list}. Cannot use ${asset}.${swapHint}`
1148
1149
  );
1149
1150
  }
1150
1151
  }
1152
+ var SENDABLE_ASSETS = ["USDC", "USDsui", "SUI"];
1153
+ var GASLESS_STABLE_TYPES = {
1154
+ USDC: SUPPORTED_ASSETS.USDC.type,
1155
+ USDsui: SUPPORTED_ASSETS.USDsui.type
1156
+ };
1151
1157
  var T2000_OVERLAY_FEE_WALLET = process.env.T2000_OVERLAY_FEE_WALLET ?? "0x5366efbf2b4fe5767fe2e78eb197aa5f5d138d88ac3333fbf3f80a1927da473a";
1152
1158
  var DEFAULT_NETWORK = "mainnet";
1153
1159
  var DEFAULT_RPC_URL = "https://fullnode.mainnet.sui.io:443";
1160
+ var DEFAULT_GRPC_URL = "https://fullnode.mainnet.sui.io:443";
1154
1161
  var DEFAULT_KEY_PATH = "~/.t2000/wallet.key";
1162
+ var GASLESS_MIN_STABLE_AMOUNT = 0.01;
1155
1163
  process.env.T2000_API_URL ?? "https://api.t2000.ai";
1156
1164
  var CETUS_USDC_SUI_POOL = "0x51e883ba7c0b566a26cbc8a94cd33eb0abd418a77cc1e60ad22fd9b1f29cd2ab";
1157
1165
  var GAS_RESERVE_MIN = 0.05;
1158
1166
 
1159
1167
  // src/utils/sui.ts
1160
1168
  init_errors();
1161
- var cachedClient = null;
1169
+ function resolveRpcUrl(rpcUrl) {
1170
+ if (rpcUrl) return rpcUrl;
1171
+ const envUrl = process.env.T2000_RPC_URL?.trim();
1172
+ if (envUrl) return envUrl;
1173
+ return DEFAULT_RPC_URL;
1174
+ }
1175
+ function resolveGrpcUrl(grpcUrl) {
1176
+ if (grpcUrl) return grpcUrl;
1177
+ const envUrl = process.env.T2000_GRPC_URL?.trim();
1178
+ if (envUrl) return envUrl;
1179
+ return DEFAULT_GRPC_URL;
1180
+ }
1181
+ var rpcClientCache = /* @__PURE__ */ new Map();
1162
1182
  function getSuiClient(rpcUrl) {
1163
- const url = rpcUrl ?? DEFAULT_RPC_URL;
1164
- if (cachedClient) return cachedClient;
1165
- cachedClient = new jsonRpc.SuiJsonRpcClient({ url, network: "mainnet" });
1166
- return cachedClient;
1183
+ const url = resolveRpcUrl(rpcUrl);
1184
+ const cached = rpcClientCache.get(url);
1185
+ if (cached) return cached;
1186
+ const client = new jsonRpc.SuiJsonRpcClient({ url, network: "mainnet" });
1187
+ rpcClientCache.set(url, client);
1188
+ return client;
1189
+ }
1190
+ function createSuiClient(network = "mainnet") {
1191
+ return new jsonRpc.SuiJsonRpcClient({ url: jsonRpc.getJsonRpcFullnodeUrl(network), network });
1192
+ }
1193
+ var grpcClientCache = /* @__PURE__ */ new Map();
1194
+ function getSuiGrpcClient(grpcUrl) {
1195
+ const baseUrl = resolveGrpcUrl(grpcUrl);
1196
+ const cached = grpcClientCache.get(baseUrl);
1197
+ if (cached) return cached;
1198
+ const client = new grpc.SuiGrpcClient({ baseUrl, network: "mainnet" });
1199
+ grpcClientCache.set(baseUrl, client);
1200
+ return client;
1167
1201
  }
1168
1202
  function validateAddress(address) {
1169
1203
  const normalized = utils.normalizeSuiAddress(address);
@@ -1186,49 +1220,10 @@ function normalizeCoinType(coinType) {
1186
1220
 
1187
1221
  // src/wallet/keyManager.ts
1188
1222
  init_errors();
1189
- var ALGORITHM = "aes-256-gcm";
1190
- var SCRYPT_N = 2 ** 14;
1191
- var SCRYPT_R = 8;
1192
- var SCRYPT_P = 1;
1193
- var SALT_LENGTH = 32;
1194
- var IV_LENGTH = 16;
1195
1223
  function expandPath(p) {
1196
1224
  if (p.startsWith("~")) return path.resolve(os.homedir(), p.slice(2));
1197
1225
  return path.resolve(p);
1198
1226
  }
1199
- function deriveKey(passphrase, salt) {
1200
- return crypto$1.scryptSync(passphrase, salt, 32, { N: SCRYPT_N, r: SCRYPT_R, p: SCRYPT_P });
1201
- }
1202
- function encrypt(data, passphrase) {
1203
- const salt = crypto$1.randomBytes(SALT_LENGTH);
1204
- const key = deriveKey(passphrase, salt);
1205
- const iv = crypto$1.randomBytes(IV_LENGTH);
1206
- const cipher = crypto$1.createCipheriv(ALGORITHM, key, iv);
1207
- const ciphertext = Buffer.concat([cipher.update(data), cipher.final()]);
1208
- const tag = cipher.getAuthTag();
1209
- return {
1210
- version: 1,
1211
- algorithm: ALGORITHM,
1212
- salt: salt.toString("hex"),
1213
- iv: iv.toString("hex"),
1214
- tag: tag.toString("hex"),
1215
- ciphertext: ciphertext.toString("hex")
1216
- };
1217
- }
1218
- function decrypt(encrypted, passphrase) {
1219
- const salt = Buffer.from(encrypted.salt, "hex");
1220
- const key = deriveKey(passphrase, salt);
1221
- const iv = Buffer.from(encrypted.iv, "hex");
1222
- const tag = Buffer.from(encrypted.tag, "hex");
1223
- const ciphertext = Buffer.from(encrypted.ciphertext, "hex");
1224
- const decipher = crypto$1.createDecipheriv(ALGORITHM, key, iv);
1225
- decipher.setAuthTag(tag);
1226
- try {
1227
- return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
1228
- } catch {
1229
- throw new exports.T2000Error("WALLET_LOCKED", "Invalid PIN");
1230
- }
1231
- }
1232
1227
  function generateKeypair() {
1233
1228
  return ed25519.Ed25519Keypair.generate();
1234
1229
  }
@@ -1240,7 +1235,30 @@ function keypairFromPrivateKey(privateKey) {
1240
1235
  const bytes = Buffer.from(privateKey.replace(/^0x/, ""), "hex");
1241
1236
  return ed25519.Ed25519Keypair.fromSecretKey(bytes);
1242
1237
  }
1243
- async function saveKey(keypair, passphrase, keyPath) {
1238
+ async function saveKey(keypair, _passphrase, keyPath) {
1239
+ const filePath = expandPath(keyPath ?? DEFAULT_KEY_PATH);
1240
+ try {
1241
+ await promises.access(filePath);
1242
+ throw new exports.T2000Error("WALLET_EXISTS", `Wallet already exists at ${filePath}`);
1243
+ } catch (error) {
1244
+ if (error instanceof exports.T2000Error) throw error;
1245
+ }
1246
+ await promises.mkdir(path.dirname(filePath), { recursive: true });
1247
+ const payload = {
1248
+ version: 2,
1249
+ secret: keypair.getSecretKey()
1250
+ };
1251
+ await promises.writeFile(filePath, JSON.stringify(payload, null, 2), { mode: 384 });
1252
+ return filePath;
1253
+ }
1254
+ async function saveBech32(secret, keyPath) {
1255
+ if (!secret.startsWith("suiprivkey")) {
1256
+ throw new exports.T2000Error(
1257
+ "INVALID_KEY",
1258
+ `Secret must be a Bech32 suiprivkey1... string. Got: ${secret.slice(0, 12)}...`
1259
+ );
1260
+ }
1261
+ cryptography.decodeSuiPrivateKey(secret);
1244
1262
  const filePath = expandPath(keyPath ?? DEFAULT_KEY_PATH);
1245
1263
  try {
1246
1264
  await promises.access(filePath);
@@ -1249,12 +1267,11 @@ async function saveKey(keypair, passphrase, keyPath) {
1249
1267
  if (error instanceof exports.T2000Error) throw error;
1250
1268
  }
1251
1269
  await promises.mkdir(path.dirname(filePath), { recursive: true });
1252
- const bech32Key = keypair.getSecretKey();
1253
- const encrypted = encrypt(Buffer.from(bech32Key, "utf-8"), passphrase);
1254
- await promises.writeFile(filePath, JSON.stringify(encrypted, null, 2), { mode: 384 });
1270
+ const payload = { version: 2, secret };
1271
+ await promises.writeFile(filePath, JSON.stringify(payload, null, 2), { mode: 384 });
1255
1272
  return filePath;
1256
1273
  }
1257
- async function loadKey(passphrase, keyPath) {
1274
+ async function loadKey(_passphrase, keyPath) {
1258
1275
  const filePath = expandPath(keyPath ?? DEFAULT_KEY_PATH);
1259
1276
  let content;
1260
1277
  try {
@@ -1262,10 +1279,22 @@ async function loadKey(passphrase, keyPath) {
1262
1279
  } catch {
1263
1280
  throw new exports.T2000Error("WALLET_NOT_FOUND", `No wallet found at ${filePath}`);
1264
1281
  }
1265
- const encrypted = JSON.parse(content);
1266
- const decrypted = decrypt(encrypted, passphrase);
1267
- const bech32Key = decrypted.toString("utf-8");
1268
- const decoded = cryptography.decodeSuiPrivateKey(bech32Key);
1282
+ let parsed;
1283
+ try {
1284
+ parsed = JSON.parse(content);
1285
+ } catch {
1286
+ throw new exports.T2000Error(
1287
+ "WALLET_CORRUPT",
1288
+ `Wallet file at ${filePath} is not a valid v4 wallet. Move or delete the file, then run \`t2 init\`.`
1289
+ );
1290
+ }
1291
+ if (!isPlainKey(parsed)) {
1292
+ throw new exports.T2000Error(
1293
+ "WALLET_CORRUPT",
1294
+ `Wallet file at ${filePath} is not a valid v4 wallet. Expected { version: 2, secret: "suiprivkey1..." }. Move or delete the file, then run \`t2 init\`.`
1295
+ );
1296
+ }
1297
+ const decoded = cryptography.decodeSuiPrivateKey(parsed.secret);
1269
1298
  return ed25519.Ed25519Keypair.fromSecretKey(decoded.secretKey);
1270
1299
  }
1271
1300
  async function walletExists(keyPath) {
@@ -1283,6 +1312,9 @@ function exportPrivateKey(keypair) {
1283
1312
  function getAddress(keypair) {
1284
1313
  return keypair.getPublicKey().toSuiAddress();
1285
1314
  }
1315
+ function isPlainKey(value) {
1316
+ return typeof value === "object" && value !== null && value.version === 2 && typeof value.secret === "string";
1317
+ }
1286
1318
 
1287
1319
  // src/wallet/keypairSigner.ts
1288
1320
  var KeypairSigner = class {
@@ -1390,12 +1422,19 @@ async function buildSendTx({
1390
1422
  address,
1391
1423
  to,
1392
1424
  amount,
1393
- asset = "USDC"
1425
+ asset
1394
1426
  }) {
1427
+ assertAllowedAsset("send", asset);
1395
1428
  const recipient = validateAddress(to);
1396
1429
  const assetInfo = SUPPORTED_ASSETS[asset];
1397
1430
  if (!assetInfo) throw new exports.T2000Error("ASSET_NOT_SUPPORTED", `Asset ${asset} is not supported`);
1398
1431
  if (amount <= 0) throw new exports.T2000Error("INVALID_AMOUNT", "Amount must be greater than zero");
1432
+ if ((asset === "USDC" || asset === "USDsui") && amount < GASLESS_MIN_STABLE_AMOUNT) {
1433
+ throw new exports.T2000Error(
1434
+ "INVALID_AMOUNT",
1435
+ `Minimum gasless transfer is ${GASLESS_MIN_STABLE_AMOUNT} ${asset}. Got ${amount}.`
1436
+ );
1437
+ }
1399
1438
  const rawAmount = displayToRaw(amount, assetInfo.decimals);
1400
1439
  const tx = new transactions.Transaction();
1401
1440
  tx.setSender(address);
@@ -1407,8 +1446,20 @@ async function buildSendTx({
1407
1446
  required: amount
1408
1447
  });
1409
1448
  }
1410
- const sendCoin = asset === "SUI" ? tx.splitCoins(tx.gas, [rawAmount])[0] : transactions.coinWithBalance({ type: assetInfo.type, balance: rawAmount })(tx);
1411
- tx.transferObjects([sendCoin], recipient);
1449
+ if (asset === "SUI") {
1450
+ const [sendCoin] = tx.splitCoins(tx.gas, [rawAmount]);
1451
+ tx.transferObjects([sendCoin], recipient);
1452
+ return tx;
1453
+ }
1454
+ const coinType = GASLESS_STABLE_TYPES[asset];
1455
+ tx.moveCall({
1456
+ target: "0x2::balance::send_funds",
1457
+ typeArguments: [coinType],
1458
+ arguments: [
1459
+ tx.balance({ type: coinType, balance: rawAmount }),
1460
+ tx.pure.address(recipient)
1461
+ ]
1462
+ });
1412
1463
  return tx;
1413
1464
  }
1414
1465
  function addSendToTx(tx, coin, recipient) {
@@ -6378,8 +6429,7 @@ var ContactManager = class {
6378
6429
  "CONTACT_NOT_FOUND",
6379
6430
  `"${nameOrAddress}" is not a valid Sui address or saved contact.
6380
6431
  Use a SuiNS name (e.g. alex.sui \u2014 register at https://suins.io)
6381
- or paste the full Sui address (0x... 64 hex characters).
6382
- Legacy contact aliases: \`t2000 contacts add ${nameOrAddress} 0x...\` (deprecated).`
6432
+ or paste the full Sui address (0x... 64 hex characters).`
6383
6433
  );
6384
6434
  }
6385
6435
  validateName(name) {
@@ -6451,20 +6501,16 @@ var T2000 = class _T2000 extends eventemitter3.EventEmitter {
6451
6501
  return registry;
6452
6502
  }
6453
6503
  static async create(options = {}) {
6454
- const { keyPath, pin, passphrase, rpcUrl } = options;
6455
- const secret = pin ?? passphrase;
6504
+ const { keyPath, rpcUrl } = options;
6456
6505
  const client = getSuiClient(rpcUrl);
6457
6506
  const exists = await walletExists(keyPath);
6458
6507
  if (!exists) {
6459
6508
  throw new exports.T2000Error(
6460
6509
  "WALLET_NOT_FOUND",
6461
- "No wallet found. Run `t2000 init` to create one."
6510
+ "No wallet found. Run `t2 init` to create one."
6462
6511
  );
6463
6512
  }
6464
- if (!secret) {
6465
- throw new exports.T2000Error("WALLET_LOCKED", "PIN required to unlock wallet");
6466
- }
6467
- const keypair = await loadKey(secret, keyPath);
6513
+ const keypair = await loadKey(void 0, keyPath);
6468
6514
  return new _T2000(keypair, client, void 0, DEFAULT_CONFIG_DIR);
6469
6515
  }
6470
6516
  static fromPrivateKey(privateKey, options = {}) {
@@ -6472,10 +6518,9 @@ var T2000 = class _T2000 extends eventemitter3.EventEmitter {
6472
6518
  const client = getSuiClient(options.rpcUrl);
6473
6519
  return new _T2000(keypair, client);
6474
6520
  }
6475
- static async init(options) {
6476
- const secret = options.pin ?? options.passphrase ?? "";
6521
+ static async init(options = {}) {
6477
6522
  const keypair = generateKeypair();
6478
- await saveKey(keypair, secret, options.keyPath);
6523
+ await saveKey(keypair, void 0, options.keyPath);
6479
6524
  const client = getSuiClient();
6480
6525
  const agent = new _T2000(keypair, client, void 0, DEFAULT_CONFIG_DIR);
6481
6526
  const address = agent.address();
@@ -6503,7 +6548,7 @@ var T2000 = class _T2000 extends eventemitter3.EventEmitter {
6503
6548
  this.enforcer.check({ operation: "pay", amount: options.maxPrice ?? 1 });
6504
6549
  const { Mppx } = await import('mppx/client');
6505
6550
  const { sui, USDC } = await import('@suimpp/mpp/client');
6506
- const { SuiGrpcClient } = await import('@mysten/sui/grpc');
6551
+ const { SuiGrpcClient: SuiGrpcClient2 } = await import('@mysten/sui/grpc');
6507
6552
  const client = this.client;
6508
6553
  const signer = this._signer;
6509
6554
  const signerAddress = signer.getAddress();
@@ -6511,7 +6556,7 @@ var T2000 = class _T2000 extends eventemitter3.EventEmitter {
6511
6556
  let gasCostSui = 0;
6512
6557
  const network = client.network === "testnet" ? "testnet" : "mainnet";
6513
6558
  const grpcBaseUrl = network === "testnet" ? "https://fullnode.testnet.sui.io" : "https://fullnode.mainnet.sui.io";
6514
- const grpcClient = new SuiGrpcClient({ baseUrl: grpcBaseUrl, network });
6559
+ const grpcClient = new SuiGrpcClient2({ baseUrl: grpcBaseUrl, network });
6515
6560
  const mppx = Mppx.create({
6516
6561
  polyfill: false,
6517
6562
  methods: [sui({
@@ -6694,23 +6739,52 @@ var T2000 = class _T2000 extends eventemitter3.EventEmitter {
6694
6739
  address() {
6695
6740
  return this._address;
6696
6741
  }
6742
+ /**
6743
+ * Send `amount` of `asset` to `to` (hex address, SuiNS name, or
6744
+ * `@audric` handle / saved contact).
6745
+ *
6746
+ * [v4.0 Phase A Day 2 — SPEC_AGENT_WALLET_GREENFIELD §A]
6747
+ *
6748
+ * **Breaking changes from v3.x:**
6749
+ * - `asset` is now REQUIRED (no implicit `?? 'USDC'` default). Callers
6750
+ * must specify `'USDC' | 'USDsui' | 'SUI'`. Sending `'USDT'` /
6751
+ * `'USDe'` / `'WAL'` / `'ETH'` / `'NAVX'` / `'GOLD'` now errors
6752
+ * with `INVALID_ASSET` — swap to a stable first.
6753
+ * - USDC + USDsui builds go through `SuiGrpcClient` so the gRPC build
6754
+ * resolver auto-detects `0x2::balance::send_funds` eligibility and
6755
+ * zeros gas at simulate time. Result: **gasless USDC / USDsui sends
6756
+ * from a zero-SUI wallet.** SUI sends stay on the standard gas-paid
6757
+ * path.
6758
+ *
6759
+ * Submission stays on the JSON-RPC client (the rest of the SDK
6760
+ * expects JSON-RPC for read paths, and Sui's docs explicitly support
6761
+ * the "build via gRPC, execute via JSON-RPC" hybrid).
6762
+ */
6697
6763
  async send(params) {
6698
6764
  this.enforcer.assertNotLocked();
6699
- const asset = params.asset ?? "USDC";
6700
- if (!(asset in SUPPORTED_ASSETS)) {
6701
- throw new exports.T2000Error("ASSET_NOT_SUPPORTED", `Asset ${asset} is not supported`);
6765
+ const asset = params.asset;
6766
+ if (!asset) {
6767
+ throw new exports.T2000Error(
6768
+ "INVALID_ASSET",
6769
+ "send() requires an explicit asset. Use one of: USDC, USDsui, SUI."
6770
+ );
6702
6771
  }
6772
+ assertAllowedAsset("send", asset);
6773
+ const sendableAsset = asset;
6703
6774
  const resolved = await this.resolveRecipient(params.to);
6704
6775
  const sendAmount = params.amount;
6705
6776
  const sendTo = resolved.address;
6777
+ const useGrpc = sendableAsset === "USDC" || sendableAsset === "USDsui";
6778
+ const buildClient = useGrpc ? getSuiGrpcClient() : void 0;
6706
6779
  const gasResult = await executeTx(
6707
6780
  this.client,
6708
6781
  this._signer,
6709
- () => buildSendTx({ client: this.client, address: this._address, to: sendTo, amount: sendAmount, asset })
6782
+ () => buildSendTx({ client: this.client, address: this._address, to: sendTo, amount: sendAmount, asset: sendableAsset }),
6783
+ { buildClient }
6710
6784
  );
6711
6785
  this.enforcer.recordUsage(sendAmount);
6712
6786
  const balance = await this.balance();
6713
- this.emitBalanceChange(asset, sendAmount, "send", gasResult.digest);
6787
+ this.emitBalanceChange(sendableAsset, sendAmount, "send", gasResult.digest);
6714
6788
  return {
6715
6789
  success: true,
6716
6790
  tx: gasResult.digest,
@@ -6820,11 +6894,12 @@ var T2000 = class _T2000 extends eventemitter3.EventEmitter {
6820
6894
  };
6821
6895
  }
6822
6896
  /**
6823
- * [SPEC_AGENTIC_STACK P1 / SDK F2 (CLI rename support) 2026-05-25]
6824
- * Preferred alias of `deposit()`. The CLI surface is `t2000 fund` post-Phase 1
6825
- * (more intuitive than "deposit" which sounds like a NAVI lending action).
6826
- * `deposit()` stays as the canonical method name for back-compat; it is NOT
6827
- * deprecated. This wrapper exists so SDK consumers can mirror CLI naming.
6897
+ * [SPEC_AGENTIC_STACK P1 / SDK F2 2026-05-25; refreshed S.342 / 2026-05-26]
6898
+ * Preferred alias of `deposit()`. Was introduced to mirror the v3 `t2000 fund`
6899
+ * CLI command; the v4 CLI surface is `t2 receive` (deleted `fund` in the
6900
+ * S.332 bulk cut). `deposit()` stays as the canonical method name for
6901
+ * back-compat; `fund()` stays as a programmatic alias for audric + other
6902
+ * SDK consumers that prefer the verb.
6828
6903
  */
6829
6904
  async fund() {
6830
6905
  return this.deposit();
@@ -7159,7 +7234,7 @@ var T2000 = class _T2000 extends eventemitter3.EventEmitter {
7159
7234
  const adapter = await this.resolveLending(params.protocol, asset, "borrow");
7160
7235
  const maxResult = await adapter.maxBorrow(this._address, asset);
7161
7236
  if (maxResult.maxAmount <= 0) {
7162
- throw new exports.T2000Error("NO_COLLATERAL", "No collateral deposited. Save first with `t2000 save <amount>`.");
7237
+ throw new exports.T2000Error("NO_COLLATERAL", "No collateral deposited. Make a save deposit first.");
7163
7238
  }
7164
7239
  if (params.amount > maxResult.maxAmount) {
7165
7240
  throw new exports.T2000Error("HEALTH_FACTOR_TOO_LOW", `Max safe borrow: $${maxResult.maxAmount.toFixed(2)}. Only savings deposits count as borrowable collateral.`, {
@@ -7917,34 +7992,68 @@ var WRITE_APPENDER_REGISTRY = {
7917
7992
  },
7918
7993
  send_transfer: async (tx, input, ctx) => {
7919
7994
  const recipient = validateAddress(input.to);
7920
- const asset = input.asset ?? "USDC";
7921
- const assetInfo = SUPPORTED_ASSETS[asset];
7922
- if (!assetInfo) {
7923
- throw new exports.T2000Error("ASSET_NOT_SUPPORTED", `Asset ${asset} is not supported`);
7995
+ if (!input.asset) {
7996
+ throw new exports.T2000Error(
7997
+ "INVALID_ASSET",
7998
+ "send_transfer requires an explicit asset. Use one of: USDC, USDsui, SUI."
7999
+ );
7924
8000
  }
8001
+ assertAllowedAsset("send", input.asset);
8002
+ const asset = input.asset;
8003
+ const assetInfo = SUPPORTED_ASSETS[asset];
7925
8004
  if (input.amount <= 0) {
7926
8005
  throw new exports.T2000Error("INVALID_AMOUNT", "Send amount must be greater than zero");
7927
8006
  }
7928
8007
  const rawAmount = BigInt(Math.floor(input.amount * 10 ** assetInfo.decimals));
7929
- let coin;
7930
- let effectiveRaw;
7931
8008
  if (ctx.chainedCoin) {
7932
- coin = ctx.chainedCoin;
7933
- effectiveRaw = rawAmount;
7934
- } else if (asset === "SUI") {
8009
+ addSendToTx(tx, ctx.chainedCoin, recipient);
8010
+ return {
8011
+ preview: {
8012
+ toolName: "send_transfer",
8013
+ effectiveAmount: Number(rawAmount) / 10 ** assetInfo.decimals,
8014
+ recipient,
8015
+ asset
8016
+ }
8017
+ };
8018
+ }
8019
+ if (asset === "SUI") {
7935
8020
  const result = await selectSuiCoin(tx, ctx.client, ctx.sender, rawAmount, ctx.sponsoredContext);
7936
- coin = result.coin;
7937
- effectiveRaw = result.effectiveAmount;
7938
- } else {
7939
- const result = await selectAndSplitCoin(tx, ctx.client, ctx.sender, assetInfo.type, rawAmount);
7940
- coin = result.coin;
7941
- effectiveRaw = result.effectiveAmount;
8021
+ addSendToTx(tx, result.coin, recipient);
8022
+ return {
8023
+ preview: {
8024
+ toolName: "send_transfer",
8025
+ effectiveAmount: Number(result.effectiveAmount) / 10 ** assetInfo.decimals,
8026
+ recipient,
8027
+ asset
8028
+ }
8029
+ };
7942
8030
  }
7943
- addSendToTx(tx, coin, recipient);
8031
+ if (input.amount < GASLESS_MIN_STABLE_AMOUNT) {
8032
+ throw new exports.T2000Error(
8033
+ "INVALID_AMOUNT",
8034
+ `Minimum gasless transfer is ${GASLESS_MIN_STABLE_AMOUNT} ${asset}. Got ${input.amount}.`
8035
+ );
8036
+ }
8037
+ const balanceResp = await ctx.client.getBalance({ owner: ctx.sender, coinType: assetInfo.type });
8038
+ if (BigInt(balanceResp.totalBalance) < rawAmount) {
8039
+ throw new exports.T2000Error("INSUFFICIENT_BALANCE", `Insufficient ${asset} balance`, {
8040
+ available: Number(balanceResp.totalBalance) / 10 ** assetInfo.decimals,
8041
+ required: input.amount
8042
+ });
8043
+ }
8044
+ const coinType = GASLESS_STABLE_TYPES[asset];
8045
+ tx.moveCall({
8046
+ target: "0x2::balance::send_funds",
8047
+ typeArguments: [coinType],
8048
+ arguments: [
8049
+ tx.balance({ type: coinType, balance: rawAmount }),
8050
+ tx.pure.address(recipient)
8051
+ ]
8052
+ });
7944
8053
  return {
7945
8054
  preview: {
7946
8055
  toolName: "send_transfer",
7947
- effectiveAmount: Number(effectiveRaw) / 10 ** assetInfo.decimals,
8056
+ effectiveAmount: Number(rawAmount) / 10 ** assetInfo.decimals,
7948
8057
  recipient,
7949
8058
  asset
7950
8059
  }
@@ -8019,24 +8128,40 @@ var WRITE_APPENDER_REGISTRY = {
8019
8128
  function deriveAllowedAddressesFromPtb(tx) {
8020
8129
  const addresses = /* @__PURE__ */ new Set();
8021
8130
  const data = tx.getData();
8022
- for (const cmd of data.commands) {
8023
- const transferCmd = cmd.TransferObjects;
8024
- if (!transferCmd) continue;
8025
- const addressArg = transferCmd.address;
8026
- if (!addressArg) continue;
8027
- const addressInputIndex = addressArg.Input;
8028
- if (addressInputIndex === void 0) continue;
8029
- const input = data.inputs[addressInputIndex];
8030
- if (!input) continue;
8131
+ const addAddressFromInput = (inputIndex) => {
8132
+ if (inputIndex === void 0) return;
8133
+ const input = data.inputs[inputIndex];
8134
+ if (!input) return;
8031
8135
  const pureBytes = input.Pure?.bytes;
8032
- if (!pureBytes) continue;
8136
+ if (!pureBytes) return;
8033
8137
  try {
8034
8138
  const bytes = base64ToBytes(pureBytes);
8035
- if (bytes.length !== 32) continue;
8139
+ if (bytes.length !== 32) return;
8036
8140
  const hex = "0x" + Array.from(bytes).map((b2) => b2.toString(16).padStart(2, "0")).join("");
8037
8141
  addresses.add(hex);
8038
8142
  } catch {
8039
8143
  }
8144
+ };
8145
+ for (const cmd of data.commands) {
8146
+ const transferCmd = cmd.TransferObjects;
8147
+ if (transferCmd) {
8148
+ const addressArg = transferCmd.address;
8149
+ const addressInputIndex = addressArg?.Input;
8150
+ addAddressFromInput(addressInputIndex);
8151
+ continue;
8152
+ }
8153
+ const moveCall = cmd.MoveCall;
8154
+ if (moveCall) {
8155
+ const mc = moveCall;
8156
+ const isBalanceSendFunds = mc.module === "balance" && mc.function === "send_funds";
8157
+ const isCoinSendFunds = mc.module === "coin" && mc.function === "send_funds";
8158
+ if (isBalanceSendFunds || isCoinSendFunds) {
8159
+ const args = mc.arguments ?? [];
8160
+ const recipientArg = args[1];
8161
+ addAddressFromInput(recipientArg?.Input);
8162
+ }
8163
+ continue;
8164
+ }
8040
8165
  }
8041
8166
  return Array.from(addresses);
8042
8167
  }
@@ -8370,8 +8495,11 @@ exports.BPS_DENOMINATOR = BPS_DENOMINATOR;
8370
8495
  exports.CETUS_USDC_SUI_POOL = CETUS_USDC_SUI_POOL;
8371
8496
  exports.CLOCK_ID = CLOCK_ID;
8372
8497
  exports.ContactManager = ContactManager;
8498
+ exports.DEFAULT_GRPC_URL = DEFAULT_GRPC_URL;
8373
8499
  exports.DEFAULT_NETWORK = DEFAULT_NETWORK;
8374
8500
  exports.DEFAULT_SAFEGUARD_CONFIG = DEFAULT_SAFEGUARD_CONFIG;
8501
+ exports.GASLESS_MIN_STABLE_AMOUNT = GASLESS_MIN_STABLE_AMOUNT;
8502
+ exports.GASLESS_STABLE_TYPES = GASLESS_STABLE_TYPES;
8375
8503
  exports.GAS_RESERVE_MIN = GAS_RESERVE_MIN;
8376
8504
  exports.HF_CRITICAL_THRESHOLD = HF_CRITICAL_THRESHOLD;
8377
8505
  exports.HF_WARN_THRESHOLD = HF_WARN_THRESHOLD;
@@ -8386,6 +8514,7 @@ exports.OUTBOUND_OPS = OUTBOUND_OPS;
8386
8514
  exports.ProtocolRegistry = ProtocolRegistry;
8387
8515
  exports.SAVEABLE_ASSETS = SAVEABLE_ASSETS;
8388
8516
  exports.SAVE_FEE_BPS = SAVE_FEE_BPS;
8517
+ exports.SENDABLE_ASSETS = SENDABLE_ASSETS;
8389
8518
  exports.SPONSORED_PYTH_DEPENDENT_PROVIDERS = SPONSORED_PYTH_DEPENDENT_PROVIDERS;
8390
8519
  exports.STABLE_ASSETS = STABLE_ASSETS;
8391
8520
  exports.SUINS_NAME_REGEX = SUINS_NAME_REGEX;
@@ -8420,6 +8549,7 @@ exports.classifyAction = classifyAction;
8420
8549
  exports.classifyLabel = classifyLabel;
8421
8550
  exports.classifyTransaction = classifyTransaction;
8422
8551
  exports.composeTx = composeTx;
8552
+ exports.createSuiClient = createSuiClient;
8423
8553
  exports.deriveAllowedAddressesFromPtb = deriveAllowedAddressesFromPtb;
8424
8554
  exports.deserializeCetusRoute = deserializeCetusRoute;
8425
8555
  exports.displayHandle = displayHandle;
@@ -8445,6 +8575,8 @@ exports.getPendingRewards = getPendingRewards;
8445
8575
  exports.getPendingRewardsByAddress = getPendingRewardsByAddress;
8446
8576
  exports.getRates = getRates;
8447
8577
  exports.getSponsoredSwapProviders = getSponsoredSwapProviders;
8578
+ exports.getSuiClient = getSuiClient;
8579
+ exports.getSuiGrpcClient = getSuiGrpcClient;
8448
8580
  exports.getSwapQuote = getSwapQuote;
8449
8581
  exports.getTier = getTier;
8450
8582
  exports.isAllowedAsset = isAllowedAsset;
@@ -8473,6 +8605,7 @@ exports.resolveAddressToSuinsViaRpc = resolveAddressToSuinsViaRpc;
8473
8605
  exports.resolveSuinsViaRpc = resolveSuinsViaRpc;
8474
8606
  exports.resolveSymbol = resolveSymbol;
8475
8607
  exports.resolveTokenType = resolveTokenType;
8608
+ exports.saveBech32 = saveBech32;
8476
8609
  exports.saveKey = saveKey;
8477
8610
  exports.selectAndSplitCoin = selectAndSplitCoin;
8478
8611
  exports.selectSuiCoin = selectSuiCoin;