@t2000/cli 10.27.1 → 10.28.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
@@ -28274,28 +28274,6 @@ var SUPPORTED_ASSETS = {
28274
28274
  }
28275
28275
  };
28276
28276
  var STABLE_ASSETS = ["USDC", "USDsui"];
28277
- var OPERATION_ASSETS = {
28278
- send: ["USDC", "USDsui", "SUI"],
28279
- swap: "*"
28280
- };
28281
- function isAllowedAsset(op, asset) {
28282
- const allowed = OPERATION_ASSETS[op];
28283
- if (allowed === "*") return true;
28284
- const target = asset.toLowerCase();
28285
- return allowed.some((a) => a.toLowerCase() === target);
28286
- }
28287
- function assertAllowedAsset(op, asset) {
28288
- if (!asset) return;
28289
- if (!isAllowedAsset(op, asset)) {
28290
- const allowed = OPERATION_ASSETS[op];
28291
- const list = Array.isArray(allowed) ? allowed.join(", ") : "any";
28292
- const swapHint = op === "send" ? " Swap to USDC or USDsui first, or send SUI." : "";
28293
- throw new T2000Error(
28294
- "INVALID_ASSET",
28295
- `${op} only supports ${list}. Cannot use ${asset}.${swapHint}`
28296
- );
28297
- }
28298
- }
28299
28277
  var GASLESS_STABLE_TYPES = {
28300
28278
  USDC: SUPPORTED_ASSETS.USDC.type,
28301
28279
  USDsui: SUPPORTED_ASSETS.USDsui.type
@@ -28899,19 +28877,42 @@ for (const [key, info] of Object.entries(SUPPORTED_ASSETS)) {
28899
28877
  ASSET_LOOKUP.set(info.displayName.toUpperCase(), key);
28900
28878
  }
28901
28879
  }
28880
+ init_token_registry();
28902
28881
  init_preflight();
28903
- function preflightSend(input) {
28882
+ function classifySendAsset(asset) {
28883
+ const coinType = resolveTokenType(asset.trim());
28884
+ if (!coinType) return null;
28885
+ let normalized;
28904
28886
  try {
28905
- assertAllowedAsset("send", input.asset);
28906
- } catch (e) {
28907
- return preflightFail("INVALID_ASSET", e.message);
28887
+ normalized = normalizeStructTag(coinType);
28888
+ } catch {
28889
+ return null;
28890
+ }
28891
+ if (normalized === normalizeStructTag(GASLESS_STABLE_TYPES.USDC)) {
28892
+ return { kind: "gasless-stable", symbol: "USDC", coinType: GASLESS_STABLE_TYPES.USDC };
28893
+ }
28894
+ if (normalized === normalizeStructTag(GASLESS_STABLE_TYPES.USDsui)) {
28895
+ return { kind: "gasless-stable", symbol: "USDsui", coinType: GASLESS_STABLE_TYPES.USDsui };
28896
+ }
28897
+ if (normalized === normalizeStructTag(SUI_TYPE)) {
28898
+ return { kind: "sui", symbol: "SUI", coinType: SUI_TYPE };
28899
+ }
28900
+ return { kind: "coin", symbol: resolveSymbol(coinType), coinType };
28901
+ }
28902
+ function invalidSendAssetMessage(asset) {
28903
+ return `Unknown asset "${asset}". Use a registry symbol (USDC, USDsui, SUI, MANIFEST, \u2026) or a full coin type (0x\u2026::module::TYPE).`;
28904
+ }
28905
+ function preflightSend(input) {
28906
+ const cls = classifySendAsset(input.asset);
28907
+ if (!cls) {
28908
+ return preflightFail("INVALID_ASSET", invalidSendAssetMessage(input.asset));
28908
28909
  }
28909
28910
  const amountCheck = checkPositiveAmount(input.amount);
28910
28911
  if (!amountCheck.valid) return amountCheck;
28911
- if ((input.asset === "USDC" || input.asset === "USDsui") && input.amount < GASLESS_MIN_STABLE_AMOUNT) {
28912
+ if (cls.kind === "gasless-stable" && input.amount < GASLESS_MIN_STABLE_AMOUNT) {
28912
28913
  return preflightFail(
28913
28914
  "INVALID_AMOUNT",
28914
- `Minimum gasless transfer is ${GASLESS_MIN_STABLE_AMOUNT} ${input.asset}. Got ${input.amount}.`
28915
+ `Minimum gasless transfer is ${GASLESS_MIN_STABLE_AMOUNT} ${cls.symbol}. Got ${input.amount}.`
28915
28916
  );
28916
28917
  }
28917
28918
  const addressCheck = checkSuiAddress(input.to);
@@ -28928,42 +28929,44 @@ async function buildSendTx({
28928
28929
  const pf = preflightSend({ to, amount, asset });
28929
28930
  if (!pf.valid) throw new T2000Error(pf.code, pf.error);
28930
28931
  const recipient = validateAddress(to);
28931
- const assetInfo = SUPPORTED_ASSETS[asset];
28932
- if (!assetInfo) throw new T2000Error("ASSET_NOT_SUPPORTED", `Asset ${asset} is not supported`);
28933
- const rawAmount = displayToRaw(amount, assetInfo.decimals);
28932
+ const cls = classifySendAsset(asset);
28933
+ const decimals = cls.kind === "gasless-stable" || cls.kind === "sui" ? SUPPORTED_ASSETS[cls.symbol].decimals : await resolveCoinDecimals(client, cls.coinType);
28934
+ const rawAmount = displayToRaw(amount, decimals);
28934
28935
  const tx = new Transaction();
28935
28936
  tx.setSender(address);
28936
- const balanceResp = await client.core.getBalance({ owner: address, coinType: assetInfo.type });
28937
+ const balanceResp = await client.core.getBalance({ owner: address, coinType: cls.coinType });
28937
28938
  const totalBalance = BigInt(balanceResp.balance.balance);
28938
28939
  if (totalBalance < rawAmount) {
28939
- throw new T2000Error("INSUFFICIENT_BALANCE", `Insufficient ${asset} balance`, {
28940
- available: Number(totalBalance) / 10 ** assetInfo.decimals,
28940
+ throw new T2000Error("INSUFFICIENT_BALANCE", `Insufficient ${cls.symbol} balance`, {
28941
+ available: Number(totalBalance) / 10 ** decimals,
28941
28942
  required: amount
28942
28943
  });
28943
28944
  }
28944
- if (asset === "USDC" || asset === "USDsui") {
28945
- const rawFloor = displayToRaw(GASLESS_MIN_STABLE_AMOUNT, assetInfo.decimals);
28946
- const remainder = totalBalance - rawAmount;
28947
- if (remainder > 0n && remainder < rawFloor) {
28948
- const total = Number(totalBalance) / 10 ** assetInfo.decimals;
28949
- throw new T2000Error(
28950
- "INVALID_AMOUNT",
28951
- `Gasless ${asset} transfers must send the entire balance or leave at least ${GASLESS_MIN_STABLE_AMOUNT} ${asset}. Sending ${amount} of ${total} leaves ${(total - amount).toFixed(assetInfo.decimals)}. Send ${total} (everything) or at most ${(total - GASLESS_MIN_STABLE_AMOUNT).toFixed(assetInfo.decimals)}.`,
28952
- { available: total, required: amount }
28953
- );
28954
- }
28955
- }
28956
- if (asset === "SUI") {
28945
+ if (cls.kind === "sui") {
28957
28946
  const [sendCoin] = tx.splitCoins(tx.gas, [rawAmount]);
28958
28947
  tx.transferObjects([sendCoin], recipient);
28959
28948
  return tx;
28960
28949
  }
28961
- const coinType = GASLESS_STABLE_TYPES[asset];
28950
+ if (cls.kind === "coin") {
28951
+ const sendCoin = coinWithBalance({ type: cls.coinType, balance: rawAmount })(tx);
28952
+ tx.transferObjects([sendCoin], recipient);
28953
+ return tx;
28954
+ }
28955
+ const rawFloor = displayToRaw(GASLESS_MIN_STABLE_AMOUNT, decimals);
28956
+ const remainder = totalBalance - rawAmount;
28957
+ if (remainder > 0n && remainder < rawFloor) {
28958
+ const total = Number(totalBalance) / 10 ** decimals;
28959
+ throw new T2000Error(
28960
+ "INVALID_AMOUNT",
28961
+ `Gasless ${cls.symbol} transfers must send the entire balance or leave at least ${GASLESS_MIN_STABLE_AMOUNT} ${cls.symbol}. Sending ${amount} of ${total} leaves ${(total - amount).toFixed(decimals)}. Send ${total} (everything) or at most ${(total - GASLESS_MIN_STABLE_AMOUNT).toFixed(decimals)}.`,
28962
+ { available: total, required: amount }
28963
+ );
28964
+ }
28962
28965
  tx.moveCall({
28963
28966
  target: "0x2::balance::send_funds",
28964
- typeArguments: [coinType],
28967
+ typeArguments: [cls.coinType],
28965
28968
  arguments: [
28966
- tx.balance({ type: coinType, balance: rawAmount }),
28969
+ tx.balance({ type: cls.coinType, balance: rawAmount }),
28967
28970
  tx.pure.address(recipient)
28968
28971
  ]
28969
28972
  });
@@ -30191,11 +30194,21 @@ var T2000 = class _T2000 extends import_index2.default {
30191
30194
  const rawAmount = BigInt(Math.floor(params.amount * 10 ** fromDecimals));
30192
30195
  let route;
30193
30196
  if (params.serializedRoute) {
30194
- const s = params.serializedRoute;
30197
+ let s = params.serializedRoute;
30198
+ const nested = s.serializedRoute;
30199
+ if (nested && typeof nested === "object" && typeof nested.fromCoinType === "string" && nested.routerData) {
30200
+ s = nested;
30201
+ }
30202
+ if (typeof s.fromCoinType !== "string" || typeof s.toCoinType !== "string") {
30203
+ throw new T2000Error(
30204
+ "SWAP_ROUTE_MISMATCH",
30205
+ "serializedRoute is not the object t2000 quoted \u2014 it is missing fromCoinType/toCoinType. Pass quote.serializedRoute exactly as the quote returned it (not the whole quote response), or omit serializedRoute to discover a fresh route."
30206
+ );
30207
+ }
30195
30208
  if (!verifyCetusRouteCoinMatch2(s, { fromCoinType: fromType, toCoinType: toType })) {
30196
30209
  throw new T2000Error(
30197
30210
  "SWAP_ROUTE_MISMATCH",
30198
- `The quoted route is for ${s.fromCoinType} -> ${s.toCoinType}, not ${params.from} -> ${params.to}. Re-quote and pass the new serializedRoute.`
30211
+ `The quoted route is for ${s.fromCoinType} -> ${s.toCoinType}, not ${fromType} -> ${toType}. Re-quote and pass the new serializedRoute.`
30199
30212
  );
30200
30213
  }
30201
30214
  if (s.byAmountIn !== byAmountIn || BigInt(s.amountIn) !== rawAmount) {
@@ -30339,18 +30352,17 @@ var T2000 = class _T2000 extends import_index2.default {
30339
30352
  /**
30340
30353
  * Send `amount` of `asset` to `to` (hex address or SuiNS name).
30341
30354
  *
30342
- * [v4.0 Phase A Day 2SPEC_AGENT_WALLET_GREENFIELD §A]
30355
+ * [S.957 2026-08-08] `asset` accepts **any held coin type** a
30356
+ * registry symbol (`USDC`, `MANIFEST`) or a full `0x…::module::TYPE`.
30357
+ * Unresolvable strings throw `INVALID_ASSET`; empty balances throw
30358
+ * `INSUFFICIENT_BALANCE`.
30343
30359
  *
30344
- * **Breaking changes from v3.x:**
30345
- * - `asset` is now REQUIRED (no implicit `?? 'USDC'` default). Callers
30346
- * must specify `'USDC' | 'USDsui' | 'SUI'`. Sending `'USDT'` /
30347
- * `'USDe'` / `'WAL'` / `'ETH'` / `'NAVX'` / `'GOLD'` now errors
30348
- * with `INVALID_ASSET` — swap to a stable first.
30349
30360
  * - USDC + USDsui builds go through `SuiGrpcClient` so the gRPC build
30350
30361
  * resolver auto-detects `0x2::balance::send_funds` eligibility and
30351
- * zeros gas at simulate time. Result: **gasless USDC / USDsui sends
30352
- * from a zero-SUI wallet.** SUI sends stay on the standard gas-paid
30353
- * path.
30362
+ * zeros gas at simulate time **gasless sends from a zero-SUI
30363
+ * wallet.** They remain the ONLY gasless assets.
30364
+ * - SUI and every other coin type are gas-paid (the sender needs SUI).
30365
+ * - `asset` stays REQUIRED (no implicit `?? 'USDC'` default — v4 rule).
30354
30366
  *
30355
30367
  * Submission stays on the JSON-RPC client (the rest of the SDK
30356
30368
  * expects JSON-RPC for read paths, and Sui's docs explicitly support
@@ -30361,30 +30373,31 @@ var T2000 = class _T2000 extends import_index2.default {
30361
30373
  if (!asset) {
30362
30374
  throw new T2000Error(
30363
30375
  "INVALID_ASSET",
30364
- "send() requires an explicit asset. Use one of: USDC, USDsui, SUI."
30376
+ "send() requires an explicit asset. Use a registry symbol (USDC, USDsui, SUI, MANIFEST, \u2026) or a full coin type (0x\u2026::module::TYPE)."
30365
30377
  );
30366
30378
  }
30367
- assertAllowedAsset("send", asset);
30368
- const sendableAsset = asset;
30379
+ const cls = classifySendAsset(asset);
30380
+ if (!cls) {
30381
+ throw new T2000Error("INVALID_ASSET", invalidSendAssetMessage(asset));
30382
+ }
30369
30383
  this.limits.assert({
30370
30384
  operation: "send",
30371
- amountUsd: approxUsdValue(sendableAsset, params.amount) ?? 0,
30385
+ amountUsd: approxUsdValue(cls.symbol, params.amount) ?? 0,
30372
30386
  force: params.force
30373
30387
  });
30374
30388
  const resolved = await this.resolveRecipient(params.to);
30375
30389
  const sendAmount = params.amount;
30376
30390
  const sendTo = resolved.address;
30377
- const useGrpc = sendableAsset === "USDC" || sendableAsset === "USDsui";
30378
- const buildClient = useGrpc ? getSuiGrpcClient() : void 0;
30391
+ const buildClient = cls.kind === "gasless-stable" ? getSuiGrpcClient() : void 0;
30379
30392
  const gasResult = await executeTx(
30380
30393
  this.client,
30381
30394
  this._signer,
30382
- () => buildSendTx({ client: this.client, address: this._address, to: sendTo, amount: sendAmount, asset: sendableAsset }),
30395
+ () => buildSendTx({ client: this.client, address: this._address, to: sendTo, amount: sendAmount, asset }),
30383
30396
  { buildClient }
30384
30397
  );
30385
- this.limits.record(approxUsdValue(sendableAsset, sendAmount) ?? 0);
30398
+ this.limits.record(approxUsdValue(cls.symbol, sendAmount) ?? 0);
30386
30399
  const balance = await this.balance();
30387
- this.emitBalanceChange(sendableAsset, sendAmount, "send", gasResult.digest);
30400
+ this.emitBalanceChange(cls.symbol, sendAmount, "send", gasResult.digest);
30388
30401
  return {
30389
30402
  success: true,
30390
30403
  tx: gasResult.digest,
@@ -34094,20 +34107,19 @@ function registerStatus(program3) {
34094
34107
 
34095
34108
  // src/commands/send.ts
34096
34109
  var import_picocolors6 = __toESM(require_picocolors(), 1);
34097
- var ACCEPTED_ASSETS = ["USDC", "USDsui", "SUI"];
34098
- var ACCEPTED_ASSETS_LIST = ACCEPTED_ASSETS.join(", ");
34110
+ var ASSET_HINT = "a registry symbol (USDC, USDsui, SUI, MANIFEST, \u2026) or a full coin type (0x\u2026::module::TYPE)";
34099
34111
  function parseSendArgs(args) {
34100
34112
  const filtered = args.filter((a) => a.toLowerCase() !== "to");
34101
34113
  if (filtered.length < 2) {
34102
34114
  throw new Error(
34103
34115
  `Usage: t2 send <amount> <asset> <recipient>
34104
- asset must be one of: ${ACCEPTED_ASSETS_LIST}
34116
+ asset is ${ASSET_HINT}
34105
34117
  recipient can be a 0x address or SuiNS name (alice.sui, alice.audric.sui)`
34106
34118
  );
34107
34119
  }
34108
34120
  if (filtered.length === 2) {
34109
34121
  throw new Error(
34110
- `Missing required <asset> argument. Use one of: ${ACCEPTED_ASSETS_LIST}. Example: t2 send ${filtered[0]} USDC ${filtered[1]}`
34122
+ `Missing required <asset> argument. Use ${ASSET_HINT}. Example: t2 send ${filtered[0]} USDC ${filtered[1]}`
34111
34123
  );
34112
34124
  }
34113
34125
  const amount = parseFloat(filtered[0]);
@@ -34115,36 +34127,34 @@ function parseSendArgs(args) {
34115
34127
  throw new Error(`Amount must be a positive number (got "${filtered[0]}").`);
34116
34128
  }
34117
34129
  const candidate = filtered[1];
34118
- const normalized = normalizeAssetSymbol(candidate);
34119
- if (!normalized) {
34130
+ const cls = classifySendAsset(candidate);
34131
+ if (!cls) {
34120
34132
  throw new Error(
34121
- `Unsupported asset "${candidate}". Use one of: ${ACCEPTED_ASSETS_LIST}. Swap to USDC or USDsui first with \`t2 swap\`, or send SUI.`
34133
+ `Unknown asset "${candidate}". Use ${ASSET_HINT}. Check your holdings with \`t2 balance\`.`
34122
34134
  );
34123
34135
  }
34124
34136
  const recipient = filtered[2];
34125
34137
  if (!recipient) {
34126
34138
  throw new Error(`Missing recipient. Usage: t2 send <amount> <asset> <recipient>.`);
34127
34139
  }
34128
- return { amount, asset: normalized, recipient };
34129
- }
34130
- function normalizeAssetSymbol(input) {
34131
- const lower = input.toLowerCase();
34132
- if (lower === "usdc") return "USDC";
34133
- if (lower === "usdsui") return "USDsui";
34134
- if (lower === "sui") return "SUI";
34135
- return void 0;
34140
+ const asset = cls.kind === "gasless-stable" || cls.kind === "sui" ? cls.symbol : candidate;
34141
+ return { amount, asset, recipient };
34136
34142
  }
34137
34143
  function registerSend(program3) {
34138
34144
  program3.command("send").argument("<amount>", "Amount of <asset> to send (denominated in asset units, NOT USD)").argument(
34139
34145
  "[args...]",
34140
- 'Asset (USDC | USDsui | SUI), optional "to" keyword, and recipient (0x address or SuiNS name like alice.sui)'
34141
- ).description("Send USDC, USDsui, or SUI. USDC + USDsui are gasless (no SUI required).").option("--key <path>", "Custom wallet path (default ~/.t2000/wallet.key)").option("--force", "Override spending limits for this call (see `t2 limit`)").addHelpText(
34146
+ 'Asset (registry symbol like USDC / MANIFEST, or a full 0x\u2026::module::TYPE coin type), optional "to" keyword, and recipient (0x address or SuiNS name like alice.sui)'
34147
+ ).description(
34148
+ "Send any held token. USDC + USDsui are gasless; everything else needs SUI for gas."
34149
+ ).option("--key <path>", "Custom wallet path (default ~/.t2000/wallet.key)").option("--force", "Override spending limits for this call (see `t2 limit`)").addHelpText(
34142
34150
  "after",
34143
34151
  `
34144
34152
  Examples:
34145
34153
  $ t2 send 5 USDC 0xabc\u2026 Send 5 USDC (gasless) to a hex address
34146
34154
  $ t2 send 5 USDsui alice.sui Send 5 USDsui (gasless) to a SuiNS name
34147
34155
  $ t2 send 0.1 SUI alice.audric.sui Send 0.1 SUI (gas required) to a SuiNS subname
34156
+ $ t2 send 10 MANIFEST 0xabc\u2026 Send any held token (gas required)
34157
+ $ t2 send 10 0xc466\u2026::manifest::MANIFEST 0xabc\u2026 Full coin type works too
34148
34158
  `
34149
34159
  ).action(async (amount, args, opts) => {
34150
34160
  try {
@@ -34153,9 +34163,8 @@ Examples:
34153
34163
  const result = await agent.send({
34154
34164
  to: recipient,
34155
34165
  amount: parsedAmount,
34156
- // The CLI parser already narrowed asset to USDC / USDsui / SUI;
34157
- // the SDK accepts `SupportedAsset` and re-validates via
34158
- // `assertAllowedAsset('send', …)` at runtime.
34166
+ // [S.957] Any resolvable asset string the SDK re-resolves via
34167
+ // `classifySendAsset` (same rule the parser used).
34159
34168
  asset,
34160
34169
  force: opts.force
34161
34170
  });
@@ -34168,7 +34177,7 @@ Examples:
34168
34177
  return;
34169
34178
  }
34170
34179
  const displayTo = result.suinsName ? `${result.suinsName} ${import_picocolors6.default.dim(`(${truncateAddress(result.to)})`)}` : truncateAddress(result.to);
34171
- const amountDisplay = asset === "SUI" ? `${result.amount.toFixed(4)} SUI` : `${formatUsd(result.amount)} ${asset}`;
34180
+ const amountDisplay = asset === "USDC" || asset === "USDsui" ? `${formatUsd(result.amount)} ${asset}` : asset === "SUI" ? `${result.amount.toFixed(4)} SUI` : `${result.amount} ${asset}`;
34172
34181
  printBlank();
34173
34182
  printSuccess(`Sent ${amountDisplay} \u2192 ${displayTo}`);
34174
34183
  if (result.gasCost === 0) {