@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.js CHANGED
@@ -4,10 +4,10 @@ import BN from 'bn.js';
4
4
  import { EventEmitter } from 'eventemitter3';
5
5
  import { createPaymentTransactionUri } from '@mysten/payment-kit';
6
6
  import { SuiJsonRpcClient, getJsonRpcFullnodeUrl } from '@mysten/sui/jsonRpc';
7
+ import { SuiGrpcClient } from '@mysten/sui/grpc';
7
8
  import { normalizeSuiAddress, isValidSuiAddress, normalizeStructTag } from '@mysten/sui/utils';
8
9
  import { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519';
9
10
  import { decodeSuiPrivateKey } from '@mysten/sui/cryptography';
10
- import { randomBytes, createCipheriv, createDecipheriv, scryptSync } from 'crypto';
11
11
  import { access, mkdir, writeFile, readFile } from 'fs/promises';
12
12
  import { join as join$1, dirname, resolve } from 'path';
13
13
  import { homedir } from 'os';
@@ -1122,7 +1122,7 @@ var OPERATION_ASSETS = {
1122
1122
  borrow: ["USDC", "USDsui"],
1123
1123
  withdraw: "*",
1124
1124
  repay: "*",
1125
- send: "*",
1125
+ send: ["USDC", "USDsui", "SUI"],
1126
1126
  swap: "*"
1127
1127
  };
1128
1128
  function isAllowedAsset(op, asset) {
@@ -1136,28 +1136,62 @@ function assertAllowedAsset(op, asset) {
1136
1136
  if (!isAllowedAsset(op, asset)) {
1137
1137
  const allowed = OPERATION_ASSETS[op];
1138
1138
  const list = Array.isArray(allowed) ? allowed.join(", ") : "any";
1139
+ const swapHint = op === "save" ? " Swap to USDC or USDsui first." : op === "send" ? " Swap to USDC or USDsui first, or send SUI." : "";
1139
1140
  throw new T2000Error(
1140
1141
  "INVALID_ASSET",
1141
- `${op} only supports ${list}. Cannot use ${asset}.${op === "save" ? " Swap to USDC or USDsui first." : ""}`
1142
+ `${op} only supports ${list}. Cannot use ${asset}.${swapHint}`
1142
1143
  );
1143
1144
  }
1144
1145
  }
1146
+ var SENDABLE_ASSETS = ["USDC", "USDsui", "SUI"];
1147
+ var GASLESS_STABLE_TYPES = {
1148
+ USDC: SUPPORTED_ASSETS.USDC.type,
1149
+ USDsui: SUPPORTED_ASSETS.USDsui.type
1150
+ };
1145
1151
  var T2000_OVERLAY_FEE_WALLET = process.env.T2000_OVERLAY_FEE_WALLET ?? "0x5366efbf2b4fe5767fe2e78eb197aa5f5d138d88ac3333fbf3f80a1927da473a";
1146
1152
  var DEFAULT_NETWORK = "mainnet";
1147
1153
  var DEFAULT_RPC_URL = "https://fullnode.mainnet.sui.io:443";
1154
+ var DEFAULT_GRPC_URL = "https://fullnode.mainnet.sui.io:443";
1148
1155
  var DEFAULT_KEY_PATH = "~/.t2000/wallet.key";
1156
+ var GASLESS_MIN_STABLE_AMOUNT = 0.01;
1149
1157
  process.env.T2000_API_URL ?? "https://api.t2000.ai";
1150
1158
  var CETUS_USDC_SUI_POOL = "0x51e883ba7c0b566a26cbc8a94cd33eb0abd418a77cc1e60ad22fd9b1f29cd2ab";
1151
1159
  var GAS_RESERVE_MIN = 0.05;
1152
1160
 
1153
1161
  // src/utils/sui.ts
1154
1162
  init_errors();
1155
- var cachedClient = null;
1163
+ function resolveRpcUrl(rpcUrl) {
1164
+ if (rpcUrl) return rpcUrl;
1165
+ const envUrl = process.env.T2000_RPC_URL?.trim();
1166
+ if (envUrl) return envUrl;
1167
+ return DEFAULT_RPC_URL;
1168
+ }
1169
+ function resolveGrpcUrl(grpcUrl) {
1170
+ if (grpcUrl) return grpcUrl;
1171
+ const envUrl = process.env.T2000_GRPC_URL?.trim();
1172
+ if (envUrl) return envUrl;
1173
+ return DEFAULT_GRPC_URL;
1174
+ }
1175
+ var rpcClientCache = /* @__PURE__ */ new Map();
1156
1176
  function getSuiClient(rpcUrl) {
1157
- const url = rpcUrl ?? DEFAULT_RPC_URL;
1158
- if (cachedClient) return cachedClient;
1159
- cachedClient = new SuiJsonRpcClient({ url, network: "mainnet" });
1160
- return cachedClient;
1177
+ const url = resolveRpcUrl(rpcUrl);
1178
+ const cached = rpcClientCache.get(url);
1179
+ if (cached) return cached;
1180
+ const client = new SuiJsonRpcClient({ url, network: "mainnet" });
1181
+ rpcClientCache.set(url, client);
1182
+ return client;
1183
+ }
1184
+ function createSuiClient(network = "mainnet") {
1185
+ return new SuiJsonRpcClient({ url: getJsonRpcFullnodeUrl(network), network });
1186
+ }
1187
+ var grpcClientCache = /* @__PURE__ */ new Map();
1188
+ function getSuiGrpcClient(grpcUrl) {
1189
+ const baseUrl = resolveGrpcUrl(grpcUrl);
1190
+ const cached = grpcClientCache.get(baseUrl);
1191
+ if (cached) return cached;
1192
+ const client = new SuiGrpcClient({ baseUrl, network: "mainnet" });
1193
+ grpcClientCache.set(baseUrl, client);
1194
+ return client;
1161
1195
  }
1162
1196
  function validateAddress(address) {
1163
1197
  const normalized = normalizeSuiAddress(address);
@@ -1180,49 +1214,10 @@ function normalizeCoinType(coinType) {
1180
1214
 
1181
1215
  // src/wallet/keyManager.ts
1182
1216
  init_errors();
1183
- var ALGORITHM = "aes-256-gcm";
1184
- var SCRYPT_N = 2 ** 14;
1185
- var SCRYPT_R = 8;
1186
- var SCRYPT_P = 1;
1187
- var SALT_LENGTH = 32;
1188
- var IV_LENGTH = 16;
1189
1217
  function expandPath(p) {
1190
1218
  if (p.startsWith("~")) return resolve(homedir(), p.slice(2));
1191
1219
  return resolve(p);
1192
1220
  }
1193
- function deriveKey(passphrase, salt) {
1194
- return scryptSync(passphrase, salt, 32, { N: SCRYPT_N, r: SCRYPT_R, p: SCRYPT_P });
1195
- }
1196
- function encrypt(data, passphrase) {
1197
- const salt = randomBytes(SALT_LENGTH);
1198
- const key = deriveKey(passphrase, salt);
1199
- const iv = randomBytes(IV_LENGTH);
1200
- const cipher = createCipheriv(ALGORITHM, key, iv);
1201
- const ciphertext = Buffer.concat([cipher.update(data), cipher.final()]);
1202
- const tag = cipher.getAuthTag();
1203
- return {
1204
- version: 1,
1205
- algorithm: ALGORITHM,
1206
- salt: salt.toString("hex"),
1207
- iv: iv.toString("hex"),
1208
- tag: tag.toString("hex"),
1209
- ciphertext: ciphertext.toString("hex")
1210
- };
1211
- }
1212
- function decrypt(encrypted, passphrase) {
1213
- const salt = Buffer.from(encrypted.salt, "hex");
1214
- const key = deriveKey(passphrase, salt);
1215
- const iv = Buffer.from(encrypted.iv, "hex");
1216
- const tag = Buffer.from(encrypted.tag, "hex");
1217
- const ciphertext = Buffer.from(encrypted.ciphertext, "hex");
1218
- const decipher = createDecipheriv(ALGORITHM, key, iv);
1219
- decipher.setAuthTag(tag);
1220
- try {
1221
- return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
1222
- } catch {
1223
- throw new T2000Error("WALLET_LOCKED", "Invalid PIN");
1224
- }
1225
- }
1226
1221
  function generateKeypair() {
1227
1222
  return Ed25519Keypair.generate();
1228
1223
  }
@@ -1234,7 +1229,30 @@ function keypairFromPrivateKey(privateKey) {
1234
1229
  const bytes = Buffer.from(privateKey.replace(/^0x/, ""), "hex");
1235
1230
  return Ed25519Keypair.fromSecretKey(bytes);
1236
1231
  }
1237
- async function saveKey(keypair, passphrase, keyPath) {
1232
+ async function saveKey(keypair, _passphrase, keyPath) {
1233
+ const filePath = expandPath(keyPath ?? DEFAULT_KEY_PATH);
1234
+ try {
1235
+ await access(filePath);
1236
+ throw new T2000Error("WALLET_EXISTS", `Wallet already exists at ${filePath}`);
1237
+ } catch (error) {
1238
+ if (error instanceof T2000Error) throw error;
1239
+ }
1240
+ await mkdir(dirname(filePath), { recursive: true });
1241
+ const payload = {
1242
+ version: 2,
1243
+ secret: keypair.getSecretKey()
1244
+ };
1245
+ await writeFile(filePath, JSON.stringify(payload, null, 2), { mode: 384 });
1246
+ return filePath;
1247
+ }
1248
+ async function saveBech32(secret, keyPath) {
1249
+ if (!secret.startsWith("suiprivkey")) {
1250
+ throw new T2000Error(
1251
+ "INVALID_KEY",
1252
+ `Secret must be a Bech32 suiprivkey1... string. Got: ${secret.slice(0, 12)}...`
1253
+ );
1254
+ }
1255
+ decodeSuiPrivateKey(secret);
1238
1256
  const filePath = expandPath(keyPath ?? DEFAULT_KEY_PATH);
1239
1257
  try {
1240
1258
  await access(filePath);
@@ -1243,12 +1261,11 @@ async function saveKey(keypair, passphrase, keyPath) {
1243
1261
  if (error instanceof T2000Error) throw error;
1244
1262
  }
1245
1263
  await mkdir(dirname(filePath), { recursive: true });
1246
- const bech32Key = keypair.getSecretKey();
1247
- const encrypted = encrypt(Buffer.from(bech32Key, "utf-8"), passphrase);
1248
- await writeFile(filePath, JSON.stringify(encrypted, null, 2), { mode: 384 });
1264
+ const payload = { version: 2, secret };
1265
+ await writeFile(filePath, JSON.stringify(payload, null, 2), { mode: 384 });
1249
1266
  return filePath;
1250
1267
  }
1251
- async function loadKey(passphrase, keyPath) {
1268
+ async function loadKey(_passphrase, keyPath) {
1252
1269
  const filePath = expandPath(keyPath ?? DEFAULT_KEY_PATH);
1253
1270
  let content;
1254
1271
  try {
@@ -1256,10 +1273,22 @@ async function loadKey(passphrase, keyPath) {
1256
1273
  } catch {
1257
1274
  throw new T2000Error("WALLET_NOT_FOUND", `No wallet found at ${filePath}`);
1258
1275
  }
1259
- const encrypted = JSON.parse(content);
1260
- const decrypted = decrypt(encrypted, passphrase);
1261
- const bech32Key = decrypted.toString("utf-8");
1262
- const decoded = decodeSuiPrivateKey(bech32Key);
1276
+ let parsed;
1277
+ try {
1278
+ parsed = JSON.parse(content);
1279
+ } catch {
1280
+ throw new T2000Error(
1281
+ "WALLET_CORRUPT",
1282
+ `Wallet file at ${filePath} is not a valid v4 wallet. Move or delete the file, then run \`t2 init\`.`
1283
+ );
1284
+ }
1285
+ if (!isPlainKey(parsed)) {
1286
+ throw new T2000Error(
1287
+ "WALLET_CORRUPT",
1288
+ `Wallet file at ${filePath} is not a valid v4 wallet. Expected { version: 2, secret: "suiprivkey1..." }. Move or delete the file, then run \`t2 init\`.`
1289
+ );
1290
+ }
1291
+ const decoded = decodeSuiPrivateKey(parsed.secret);
1263
1292
  return Ed25519Keypair.fromSecretKey(decoded.secretKey);
1264
1293
  }
1265
1294
  async function walletExists(keyPath) {
@@ -1277,6 +1306,9 @@ function exportPrivateKey(keypair) {
1277
1306
  function getAddress(keypair) {
1278
1307
  return keypair.getPublicKey().toSuiAddress();
1279
1308
  }
1309
+ function isPlainKey(value) {
1310
+ return typeof value === "object" && value !== null && value.version === 2 && typeof value.secret === "string";
1311
+ }
1280
1312
 
1281
1313
  // src/wallet/keypairSigner.ts
1282
1314
  var KeypairSigner = class {
@@ -1384,12 +1416,19 @@ async function buildSendTx({
1384
1416
  address,
1385
1417
  to,
1386
1418
  amount,
1387
- asset = "USDC"
1419
+ asset
1388
1420
  }) {
1421
+ assertAllowedAsset("send", asset);
1389
1422
  const recipient = validateAddress(to);
1390
1423
  const assetInfo = SUPPORTED_ASSETS[asset];
1391
1424
  if (!assetInfo) throw new T2000Error("ASSET_NOT_SUPPORTED", `Asset ${asset} is not supported`);
1392
1425
  if (amount <= 0) throw new T2000Error("INVALID_AMOUNT", "Amount must be greater than zero");
1426
+ if ((asset === "USDC" || asset === "USDsui") && amount < GASLESS_MIN_STABLE_AMOUNT) {
1427
+ throw new T2000Error(
1428
+ "INVALID_AMOUNT",
1429
+ `Minimum gasless transfer is ${GASLESS_MIN_STABLE_AMOUNT} ${asset}. Got ${amount}.`
1430
+ );
1431
+ }
1393
1432
  const rawAmount = displayToRaw(amount, assetInfo.decimals);
1394
1433
  const tx = new Transaction();
1395
1434
  tx.setSender(address);
@@ -1401,8 +1440,20 @@ async function buildSendTx({
1401
1440
  required: amount
1402
1441
  });
1403
1442
  }
1404
- const sendCoin = asset === "SUI" ? tx.splitCoins(tx.gas, [rawAmount])[0] : coinWithBalance({ type: assetInfo.type, balance: rawAmount })(tx);
1405
- tx.transferObjects([sendCoin], recipient);
1443
+ if (asset === "SUI") {
1444
+ const [sendCoin] = tx.splitCoins(tx.gas, [rawAmount]);
1445
+ tx.transferObjects([sendCoin], recipient);
1446
+ return tx;
1447
+ }
1448
+ const coinType = GASLESS_STABLE_TYPES[asset];
1449
+ tx.moveCall({
1450
+ target: "0x2::balance::send_funds",
1451
+ typeArguments: [coinType],
1452
+ arguments: [
1453
+ tx.balance({ type: coinType, balance: rawAmount }),
1454
+ tx.pure.address(recipient)
1455
+ ]
1456
+ });
1406
1457
  return tx;
1407
1458
  }
1408
1459
  function addSendToTx(tx, coin, recipient) {
@@ -6372,8 +6423,7 @@ var ContactManager = class {
6372
6423
  "CONTACT_NOT_FOUND",
6373
6424
  `"${nameOrAddress}" is not a valid Sui address or saved contact.
6374
6425
  Use a SuiNS name (e.g. alex.sui \u2014 register at https://suins.io)
6375
- or paste the full Sui address (0x... 64 hex characters).
6376
- Legacy contact aliases: \`t2000 contacts add ${nameOrAddress} 0x...\` (deprecated).`
6426
+ or paste the full Sui address (0x... 64 hex characters).`
6377
6427
  );
6378
6428
  }
6379
6429
  validateName(name) {
@@ -6445,20 +6495,16 @@ var T2000 = class _T2000 extends EventEmitter {
6445
6495
  return registry;
6446
6496
  }
6447
6497
  static async create(options = {}) {
6448
- const { keyPath, pin, passphrase, rpcUrl } = options;
6449
- const secret = pin ?? passphrase;
6498
+ const { keyPath, rpcUrl } = options;
6450
6499
  const client = getSuiClient(rpcUrl);
6451
6500
  const exists = await walletExists(keyPath);
6452
6501
  if (!exists) {
6453
6502
  throw new T2000Error(
6454
6503
  "WALLET_NOT_FOUND",
6455
- "No wallet found. Run `t2000 init` to create one."
6504
+ "No wallet found. Run `t2 init` to create one."
6456
6505
  );
6457
6506
  }
6458
- if (!secret) {
6459
- throw new T2000Error("WALLET_LOCKED", "PIN required to unlock wallet");
6460
- }
6461
- const keypair = await loadKey(secret, keyPath);
6507
+ const keypair = await loadKey(void 0, keyPath);
6462
6508
  return new _T2000(keypair, client, void 0, DEFAULT_CONFIG_DIR);
6463
6509
  }
6464
6510
  static fromPrivateKey(privateKey, options = {}) {
@@ -6466,10 +6512,9 @@ var T2000 = class _T2000 extends EventEmitter {
6466
6512
  const client = getSuiClient(options.rpcUrl);
6467
6513
  return new _T2000(keypair, client);
6468
6514
  }
6469
- static async init(options) {
6470
- const secret = options.pin ?? options.passphrase ?? "";
6515
+ static async init(options = {}) {
6471
6516
  const keypair = generateKeypair();
6472
- await saveKey(keypair, secret, options.keyPath);
6517
+ await saveKey(keypair, void 0, options.keyPath);
6473
6518
  const client = getSuiClient();
6474
6519
  const agent = new _T2000(keypair, client, void 0, DEFAULT_CONFIG_DIR);
6475
6520
  const address = agent.address();
@@ -6497,7 +6542,7 @@ var T2000 = class _T2000 extends EventEmitter {
6497
6542
  this.enforcer.check({ operation: "pay", amount: options.maxPrice ?? 1 });
6498
6543
  const { Mppx } = await import('mppx/client');
6499
6544
  const { sui, USDC } = await import('@suimpp/mpp/client');
6500
- const { SuiGrpcClient } = await import('@mysten/sui/grpc');
6545
+ const { SuiGrpcClient: SuiGrpcClient2 } = await import('@mysten/sui/grpc');
6501
6546
  const client = this.client;
6502
6547
  const signer = this._signer;
6503
6548
  const signerAddress = signer.getAddress();
@@ -6505,7 +6550,7 @@ var T2000 = class _T2000 extends EventEmitter {
6505
6550
  let gasCostSui = 0;
6506
6551
  const network = client.network === "testnet" ? "testnet" : "mainnet";
6507
6552
  const grpcBaseUrl = network === "testnet" ? "https://fullnode.testnet.sui.io" : "https://fullnode.mainnet.sui.io";
6508
- const grpcClient = new SuiGrpcClient({ baseUrl: grpcBaseUrl, network });
6553
+ const grpcClient = new SuiGrpcClient2({ baseUrl: grpcBaseUrl, network });
6509
6554
  const mppx = Mppx.create({
6510
6555
  polyfill: false,
6511
6556
  methods: [sui({
@@ -6688,23 +6733,52 @@ var T2000 = class _T2000 extends EventEmitter {
6688
6733
  address() {
6689
6734
  return this._address;
6690
6735
  }
6736
+ /**
6737
+ * Send `amount` of `asset` to `to` (hex address, SuiNS name, or
6738
+ * `@audric` handle / saved contact).
6739
+ *
6740
+ * [v4.0 Phase A Day 2 — SPEC_AGENT_WALLET_GREENFIELD §A]
6741
+ *
6742
+ * **Breaking changes from v3.x:**
6743
+ * - `asset` is now REQUIRED (no implicit `?? 'USDC'` default). Callers
6744
+ * must specify `'USDC' | 'USDsui' | 'SUI'`. Sending `'USDT'` /
6745
+ * `'USDe'` / `'WAL'` / `'ETH'` / `'NAVX'` / `'GOLD'` now errors
6746
+ * with `INVALID_ASSET` — swap to a stable first.
6747
+ * - USDC + USDsui builds go through `SuiGrpcClient` so the gRPC build
6748
+ * resolver auto-detects `0x2::balance::send_funds` eligibility and
6749
+ * zeros gas at simulate time. Result: **gasless USDC / USDsui sends
6750
+ * from a zero-SUI wallet.** SUI sends stay on the standard gas-paid
6751
+ * path.
6752
+ *
6753
+ * Submission stays on the JSON-RPC client (the rest of the SDK
6754
+ * expects JSON-RPC for read paths, and Sui's docs explicitly support
6755
+ * the "build via gRPC, execute via JSON-RPC" hybrid).
6756
+ */
6691
6757
  async send(params) {
6692
6758
  this.enforcer.assertNotLocked();
6693
- const asset = params.asset ?? "USDC";
6694
- if (!(asset in SUPPORTED_ASSETS)) {
6695
- throw new T2000Error("ASSET_NOT_SUPPORTED", `Asset ${asset} is not supported`);
6759
+ const asset = params.asset;
6760
+ if (!asset) {
6761
+ throw new T2000Error(
6762
+ "INVALID_ASSET",
6763
+ "send() requires an explicit asset. Use one of: USDC, USDsui, SUI."
6764
+ );
6696
6765
  }
6766
+ assertAllowedAsset("send", asset);
6767
+ const sendableAsset = asset;
6697
6768
  const resolved = await this.resolveRecipient(params.to);
6698
6769
  const sendAmount = params.amount;
6699
6770
  const sendTo = resolved.address;
6771
+ const useGrpc = sendableAsset === "USDC" || sendableAsset === "USDsui";
6772
+ const buildClient = useGrpc ? getSuiGrpcClient() : void 0;
6700
6773
  const gasResult = await executeTx(
6701
6774
  this.client,
6702
6775
  this._signer,
6703
- () => buildSendTx({ client: this.client, address: this._address, to: sendTo, amount: sendAmount, asset })
6776
+ () => buildSendTx({ client: this.client, address: this._address, to: sendTo, amount: sendAmount, asset: sendableAsset }),
6777
+ { buildClient }
6704
6778
  );
6705
6779
  this.enforcer.recordUsage(sendAmount);
6706
6780
  const balance = await this.balance();
6707
- this.emitBalanceChange(asset, sendAmount, "send", gasResult.digest);
6781
+ this.emitBalanceChange(sendableAsset, sendAmount, "send", gasResult.digest);
6708
6782
  return {
6709
6783
  success: true,
6710
6784
  tx: gasResult.digest,
@@ -6814,11 +6888,12 @@ var T2000 = class _T2000 extends EventEmitter {
6814
6888
  };
6815
6889
  }
6816
6890
  /**
6817
- * [SPEC_AGENTIC_STACK P1 / SDK F2 (CLI rename support) 2026-05-25]
6818
- * Preferred alias of `deposit()`. The CLI surface is `t2000 fund` post-Phase 1
6819
- * (more intuitive than "deposit" which sounds like a NAVI lending action).
6820
- * `deposit()` stays as the canonical method name for back-compat; it is NOT
6821
- * deprecated. This wrapper exists so SDK consumers can mirror CLI naming.
6891
+ * [SPEC_AGENTIC_STACK P1 / SDK F2 2026-05-25; refreshed S.342 / 2026-05-26]
6892
+ * Preferred alias of `deposit()`. Was introduced to mirror the v3 `t2000 fund`
6893
+ * CLI command; the v4 CLI surface is `t2 receive` (deleted `fund` in the
6894
+ * S.332 bulk cut). `deposit()` stays as the canonical method name for
6895
+ * back-compat; `fund()` stays as a programmatic alias for audric + other
6896
+ * SDK consumers that prefer the verb.
6822
6897
  */
6823
6898
  async fund() {
6824
6899
  return this.deposit();
@@ -7153,7 +7228,7 @@ var T2000 = class _T2000 extends EventEmitter {
7153
7228
  const adapter = await this.resolveLending(params.protocol, asset, "borrow");
7154
7229
  const maxResult = await adapter.maxBorrow(this._address, asset);
7155
7230
  if (maxResult.maxAmount <= 0) {
7156
- throw new T2000Error("NO_COLLATERAL", "No collateral deposited. Save first with `t2000 save <amount>`.");
7231
+ throw new T2000Error("NO_COLLATERAL", "No collateral deposited. Make a save deposit first.");
7157
7232
  }
7158
7233
  if (params.amount > maxResult.maxAmount) {
7159
7234
  throw new T2000Error("HEALTH_FACTOR_TOO_LOW", `Max safe borrow: $${maxResult.maxAmount.toFixed(2)}. Only savings deposits count as borrowable collateral.`, {
@@ -7911,34 +7986,68 @@ var WRITE_APPENDER_REGISTRY = {
7911
7986
  },
7912
7987
  send_transfer: async (tx, input, ctx) => {
7913
7988
  const recipient = validateAddress(input.to);
7914
- const asset = input.asset ?? "USDC";
7915
- const assetInfo = SUPPORTED_ASSETS[asset];
7916
- if (!assetInfo) {
7917
- throw new T2000Error("ASSET_NOT_SUPPORTED", `Asset ${asset} is not supported`);
7989
+ if (!input.asset) {
7990
+ throw new T2000Error(
7991
+ "INVALID_ASSET",
7992
+ "send_transfer requires an explicit asset. Use one of: USDC, USDsui, SUI."
7993
+ );
7918
7994
  }
7995
+ assertAllowedAsset("send", input.asset);
7996
+ const asset = input.asset;
7997
+ const assetInfo = SUPPORTED_ASSETS[asset];
7919
7998
  if (input.amount <= 0) {
7920
7999
  throw new T2000Error("INVALID_AMOUNT", "Send amount must be greater than zero");
7921
8000
  }
7922
8001
  const rawAmount = BigInt(Math.floor(input.amount * 10 ** assetInfo.decimals));
7923
- let coin;
7924
- let effectiveRaw;
7925
8002
  if (ctx.chainedCoin) {
7926
- coin = ctx.chainedCoin;
7927
- effectiveRaw = rawAmount;
7928
- } else if (asset === "SUI") {
8003
+ addSendToTx(tx, ctx.chainedCoin, recipient);
8004
+ return {
8005
+ preview: {
8006
+ toolName: "send_transfer",
8007
+ effectiveAmount: Number(rawAmount) / 10 ** assetInfo.decimals,
8008
+ recipient,
8009
+ asset
8010
+ }
8011
+ };
8012
+ }
8013
+ if (asset === "SUI") {
7929
8014
  const result = await selectSuiCoin(tx, ctx.client, ctx.sender, rawAmount, ctx.sponsoredContext);
7930
- coin = result.coin;
7931
- effectiveRaw = result.effectiveAmount;
7932
- } else {
7933
- const result = await selectAndSplitCoin(tx, ctx.client, ctx.sender, assetInfo.type, rawAmount);
7934
- coin = result.coin;
7935
- effectiveRaw = result.effectiveAmount;
8015
+ addSendToTx(tx, result.coin, recipient);
8016
+ return {
8017
+ preview: {
8018
+ toolName: "send_transfer",
8019
+ effectiveAmount: Number(result.effectiveAmount) / 10 ** assetInfo.decimals,
8020
+ recipient,
8021
+ asset
8022
+ }
8023
+ };
7936
8024
  }
7937
- addSendToTx(tx, coin, recipient);
8025
+ if (input.amount < GASLESS_MIN_STABLE_AMOUNT) {
8026
+ throw new T2000Error(
8027
+ "INVALID_AMOUNT",
8028
+ `Minimum gasless transfer is ${GASLESS_MIN_STABLE_AMOUNT} ${asset}. Got ${input.amount}.`
8029
+ );
8030
+ }
8031
+ const balanceResp = await ctx.client.getBalance({ owner: ctx.sender, coinType: assetInfo.type });
8032
+ if (BigInt(balanceResp.totalBalance) < rawAmount) {
8033
+ throw new T2000Error("INSUFFICIENT_BALANCE", `Insufficient ${asset} balance`, {
8034
+ available: Number(balanceResp.totalBalance) / 10 ** assetInfo.decimals,
8035
+ required: input.amount
8036
+ });
8037
+ }
8038
+ const coinType = GASLESS_STABLE_TYPES[asset];
8039
+ tx.moveCall({
8040
+ target: "0x2::balance::send_funds",
8041
+ typeArguments: [coinType],
8042
+ arguments: [
8043
+ tx.balance({ type: coinType, balance: rawAmount }),
8044
+ tx.pure.address(recipient)
8045
+ ]
8046
+ });
7938
8047
  return {
7939
8048
  preview: {
7940
8049
  toolName: "send_transfer",
7941
- effectiveAmount: Number(effectiveRaw) / 10 ** assetInfo.decimals,
8050
+ effectiveAmount: Number(rawAmount) / 10 ** assetInfo.decimals,
7942
8051
  recipient,
7943
8052
  asset
7944
8053
  }
@@ -8013,24 +8122,40 @@ var WRITE_APPENDER_REGISTRY = {
8013
8122
  function deriveAllowedAddressesFromPtb(tx) {
8014
8123
  const addresses = /* @__PURE__ */ new Set();
8015
8124
  const data = tx.getData();
8016
- for (const cmd of data.commands) {
8017
- const transferCmd = cmd.TransferObjects;
8018
- if (!transferCmd) continue;
8019
- const addressArg = transferCmd.address;
8020
- if (!addressArg) continue;
8021
- const addressInputIndex = addressArg.Input;
8022
- if (addressInputIndex === void 0) continue;
8023
- const input = data.inputs[addressInputIndex];
8024
- if (!input) continue;
8125
+ const addAddressFromInput = (inputIndex) => {
8126
+ if (inputIndex === void 0) return;
8127
+ const input = data.inputs[inputIndex];
8128
+ if (!input) return;
8025
8129
  const pureBytes = input.Pure?.bytes;
8026
- if (!pureBytes) continue;
8130
+ if (!pureBytes) return;
8027
8131
  try {
8028
8132
  const bytes = base64ToBytes(pureBytes);
8029
- if (bytes.length !== 32) continue;
8133
+ if (bytes.length !== 32) return;
8030
8134
  const hex = "0x" + Array.from(bytes).map((b2) => b2.toString(16).padStart(2, "0")).join("");
8031
8135
  addresses.add(hex);
8032
8136
  } catch {
8033
8137
  }
8138
+ };
8139
+ for (const cmd of data.commands) {
8140
+ const transferCmd = cmd.TransferObjects;
8141
+ if (transferCmd) {
8142
+ const addressArg = transferCmd.address;
8143
+ const addressInputIndex = addressArg?.Input;
8144
+ addAddressFromInput(addressInputIndex);
8145
+ continue;
8146
+ }
8147
+ const moveCall = cmd.MoveCall;
8148
+ if (moveCall) {
8149
+ const mc = moveCall;
8150
+ const isBalanceSendFunds = mc.module === "balance" && mc.function === "send_funds";
8151
+ const isCoinSendFunds = mc.module === "coin" && mc.function === "send_funds";
8152
+ if (isBalanceSendFunds || isCoinSendFunds) {
8153
+ const args = mc.arguments ?? [];
8154
+ const recipientArg = args[1];
8155
+ addAddressFromInput(recipientArg?.Input);
8156
+ }
8157
+ continue;
8158
+ }
8034
8159
  }
8035
8160
  return Array.from(addresses);
8036
8161
  }
@@ -8356,6 +8481,6 @@ function displayHandle(label) {
8356
8481
  (*! scure-base - MIT License (c) 2022 Paul Miller (paulmillr.com) *)
8357
8482
  */
8358
8483
 
8359
- export { ALL_NAVI_ASSETS, AUDRIC_PARENT_NAME, AUDRIC_PARENT_NFT_ID, BORROW_FEE_BPS, BPS_DENOMINATOR, CETUS_USDC_SUI_POOL, CLOCK_ID, COIN_REGISTRY, ContactManager, DEFAULT_NETWORK, DEFAULT_SAFEGUARD_CONFIG, ETH_TYPE, GAS_RESERVE_MIN, HF_CRITICAL_THRESHOLD, HF_WARN_THRESHOLD, IKA_TYPE, InvalidAddressError, KNOWN_TARGETS, KeypairSigner, LABEL_PATTERNS, LOFI_TYPE, MANIFEST_TYPE, MIST_PER_SUI, NAVX_TYPE, NaviAdapter, OPERATION_ASSETS, OUTBOUND_OPS, OVERLAY_FEE_RATE, ProtocolRegistry, SAVEABLE_ASSETS, SAVE_FEE_BPS, SPONSORED_PYTH_DEPENDENT_PROVIDERS, STABLE_ASSETS, SUINS_NAME_REGEX, SUI_ADDRESS_REGEX, SUI_ADDRESS_STRICT_REGEX, SUI_DECIMALS, SUI_TYPE, SUPPORTED_ASSETS, SafeguardEnforcer, SafeguardError, SuinsNotRegisteredError, SuinsRpcError, T2000, T2000Error, T2000_OVERLAY_FEE_WALLET, TOKEN_MAP, USDC_DECIMALS, USDC_TYPE, USDE_TYPE, USDSUI_TYPE, USDT_TYPE, WAL_TYPE, WBTC_TYPE, WRITE_APPENDER_REGISTRY, ZkLoginSigner, addClaimRewardsToTx, addFeeTransfer, addSendToTx, addSwapToTx, aggregateClaimableRewards, allDescriptors, assertAllowedAsset, buildAddLeafTx, buildClaimRewardsTx, buildHarvestRewardsTx, buildRevokeLeafTx, buildSendTx, buildSwapTx, calculateFee, classifyAction, classifyLabel, classifyTransaction, composeTx, deriveAllowedAddressesFromPtb, deserializeCetusRoute, displayHandle, exportPrivateKey, extractAllUserLegs, extractTransferDetails, extractTxCommands, extractTxSender, fallbackLabel, fetchAllCoins, findSwapRoute, formatAssetAmount, formatSui, formatUsd, fullHandle, generateKeypair, getAddress, getCoinMeta, getDecimals, getDecimalsForCoinType, getFinancialSummary, getPendingRewards, getPendingRewardsByAddress, getRates, getSponsoredSwapProviders, getSwapQuote, getTier, isAllowedAsset, isCetusRouteFresh, isInRegistry, isSupported, isTier1, isTier2, keypairFromPrivateKey, loadKey, looksLikeSuiNs, mapMoveAbortCode, mapWalletError, mistToSui, naviDescriptor, normalizeAddressInput, normalizeAsset, normalizeCoinType, parseSuiRpcTx, queryHistory, queryTransaction, rawToStable, rawToUsdc, refineLendingLabel, resolveAddressToSuinsViaRpc, resolveSuinsViaRpc, resolveSymbol, resolveTokenType, saveKey, selectAndSplitCoin, selectSuiCoin, serializeCetusRoute, simulateTransaction, stableToRaw, suiToMist, throwIfSimulationFailed, truncateAddress, usdcToRaw, validateAddress, validateLabel, verifyCetusRouteCoinMatch, walletExists };
8484
+ export { ALL_NAVI_ASSETS, AUDRIC_PARENT_NAME, AUDRIC_PARENT_NFT_ID, BORROW_FEE_BPS, BPS_DENOMINATOR, CETUS_USDC_SUI_POOL, CLOCK_ID, COIN_REGISTRY, ContactManager, DEFAULT_GRPC_URL, DEFAULT_NETWORK, DEFAULT_SAFEGUARD_CONFIG, ETH_TYPE, GASLESS_MIN_STABLE_AMOUNT, GASLESS_STABLE_TYPES, GAS_RESERVE_MIN, HF_CRITICAL_THRESHOLD, HF_WARN_THRESHOLD, IKA_TYPE, InvalidAddressError, KNOWN_TARGETS, KeypairSigner, LABEL_PATTERNS, LOFI_TYPE, MANIFEST_TYPE, MIST_PER_SUI, NAVX_TYPE, NaviAdapter, OPERATION_ASSETS, OUTBOUND_OPS, OVERLAY_FEE_RATE, ProtocolRegistry, SAVEABLE_ASSETS, SAVE_FEE_BPS, SENDABLE_ASSETS, SPONSORED_PYTH_DEPENDENT_PROVIDERS, STABLE_ASSETS, SUINS_NAME_REGEX, SUI_ADDRESS_REGEX, SUI_ADDRESS_STRICT_REGEX, SUI_DECIMALS, SUI_TYPE, SUPPORTED_ASSETS, SafeguardEnforcer, SafeguardError, SuinsNotRegisteredError, SuinsRpcError, T2000, T2000Error, T2000_OVERLAY_FEE_WALLET, TOKEN_MAP, USDC_DECIMALS, USDC_TYPE, USDE_TYPE, USDSUI_TYPE, USDT_TYPE, WAL_TYPE, WBTC_TYPE, WRITE_APPENDER_REGISTRY, ZkLoginSigner, addClaimRewardsToTx, addFeeTransfer, addSendToTx, addSwapToTx, aggregateClaimableRewards, allDescriptors, assertAllowedAsset, buildAddLeafTx, buildClaimRewardsTx, buildHarvestRewardsTx, buildRevokeLeafTx, buildSendTx, buildSwapTx, calculateFee, classifyAction, classifyLabel, classifyTransaction, composeTx, createSuiClient, deriveAllowedAddressesFromPtb, deserializeCetusRoute, displayHandle, exportPrivateKey, extractAllUserLegs, extractTransferDetails, extractTxCommands, extractTxSender, fallbackLabel, fetchAllCoins, findSwapRoute, formatAssetAmount, formatSui, formatUsd, fullHandle, generateKeypair, getAddress, getCoinMeta, getDecimals, getDecimalsForCoinType, getFinancialSummary, getPendingRewards, getPendingRewardsByAddress, getRates, getSponsoredSwapProviders, getSuiClient, getSuiGrpcClient, getSwapQuote, getTier, isAllowedAsset, isCetusRouteFresh, isInRegistry, isSupported, isTier1, isTier2, keypairFromPrivateKey, loadKey, looksLikeSuiNs, mapMoveAbortCode, mapWalletError, mistToSui, naviDescriptor, normalizeAddressInput, normalizeAsset, normalizeCoinType, parseSuiRpcTx, queryHistory, queryTransaction, rawToStable, rawToUsdc, refineLendingLabel, resolveAddressToSuinsViaRpc, resolveSuinsViaRpc, resolveSymbol, resolveTokenType, saveBech32, saveKey, selectAndSplitCoin, selectSuiCoin, serializeCetusRoute, simulateTransaction, stableToRaw, suiToMist, throwIfSimulationFailed, truncateAddress, usdcToRaw, validateAddress, validateLabel, verifyCetusRouteCoinMatch, walletExists };
8360
8485
  //# sourceMappingURL=index.js.map
8361
8486
  //# sourceMappingURL=index.js.map