@t2000/sdk 5.5.1 → 5.6.1

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/browser.cjs CHANGED
@@ -231,6 +231,7 @@ var init_token_registry = __esm({
231
231
  // src/wallet/coinSelection.ts
232
232
  var coinSelection_exports = {};
233
233
  __export(coinSelection_exports, {
234
+ buildCoinToAddressBalanceMigration: () => buildCoinToAddressBalanceMigration,
234
235
  fetchAllCoins: () => fetchAllCoins,
235
236
  selectAndSplitCoin: () => selectAndSplitCoin,
236
237
  selectSuiCoin: () => selectSuiCoin
@@ -344,6 +345,32 @@ async function selectCoinObjectsOnly(tx, client, owner, coinType, amount, allowS
344
345
  });
345
346
  return { coin, effectiveAmount, swapAll };
346
347
  }
348
+ function buildCoinToAddressBalanceMigration(args) {
349
+ const { coins, coinType, owner, minAmount } = args;
350
+ const sorted = [...coins].filter((c) => c.balance > 0n).sort((a, b) => b.balance > a.balance ? 1 : b.balance < a.balance ? -1 : 0);
351
+ const selected = [];
352
+ let migratedRaw = 0n;
353
+ for (const c of sorted) {
354
+ if (migratedRaw >= minAmount) break;
355
+ selected.push(c);
356
+ migratedRaw += c.balance;
357
+ }
358
+ if (migratedRaw < minAmount) {
359
+ throw new exports.T2000Error("INSUFFICIENT_BALANCE", `Insufficient ${coinType} coin objects to migrate`, {
360
+ available: migratedRaw.toString(),
361
+ required: minAmount.toString()
362
+ });
363
+ }
364
+ const tx = new transactions.Transaction();
365
+ for (const c of selected) {
366
+ tx.moveCall({
367
+ target: "0x2::coin::send_funds",
368
+ typeArguments: [coinType],
369
+ arguments: [tx.object(c.objectId), tx.pure.address(owner)]
370
+ });
371
+ }
372
+ return { tx, migratedRaw };
373
+ }
347
374
  async function selectSuiCoin(tx, client, owner, amountMist, sponsoredContext, mergeCache) {
348
375
  if (sponsoredContext) {
349
376
  const { SUI_TYPE: SUI_TYPE2 } = await Promise.resolve().then(() => (init_token_registry(), token_registry_exports));
@@ -442,10 +469,60 @@ async function executeTx(client, signer, buildTx, options = {}) {
442
469
  }
443
470
  return { digest: txn.digest, gasCostSui, effects };
444
471
  }
472
+ var PREFLIGHT_MAX_AMOUNT = 1e6;
473
+ var PREFLIGHT_OK = { valid: true };
474
+ function preflightFail(code, error) {
475
+ return { valid: false, code, error };
476
+ }
477
+ function checkPositiveAmount(amount, label = "Amount", max = PREFLIGHT_MAX_AMOUNT) {
478
+ if (typeof amount !== "number" || !Number.isFinite(amount)) {
479
+ return preflightFail("INVALID_AMOUNT", `${label} must be a finite number`);
480
+ }
481
+ if (amount <= 0) {
482
+ return preflightFail("INVALID_AMOUNT", `${label} must be greater than zero`);
483
+ }
484
+ if (amount > max) {
485
+ return preflightFail("INVALID_AMOUNT", `${label} ${amount} exceeds the sane maximum (${max})`);
486
+ }
487
+ return PREFLIGHT_OK;
488
+ }
489
+ function checkSuiAddress(address, label = "recipient") {
490
+ try {
491
+ if (typeof address === "string" && address.trim() !== "" && utils.isValidSuiAddress(utils.normalizeSuiAddress(address))) {
492
+ return PREFLIGHT_OK;
493
+ }
494
+ } catch {
495
+ }
496
+ return preflightFail("INVALID_ADDRESS", `Invalid ${label} address: ${address}`);
497
+ }
445
498
 
446
499
  // src/wallet/pay.ts
500
+ function preflightPay(input) {
501
+ if (typeof input.url !== "string" || input.url.trim() === "") {
502
+ return preflightFail("FACILITATOR_REJECTION", "A target URL is required to pay");
503
+ }
504
+ let parsed;
505
+ try {
506
+ parsed = new URL(input.url);
507
+ } catch {
508
+ return preflightFail("FACILITATOR_REJECTION", `Invalid URL: ${input.url}`);
509
+ }
510
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
511
+ return preflightFail(
512
+ "FACILITATOR_REJECTION",
513
+ `URL must be http(s): got ${parsed.protocol}//`
514
+ );
515
+ }
516
+ if (input.maxPrice !== void 0) {
517
+ const priceCheck = checkPositiveAmount(input.maxPrice, "maxPrice");
518
+ if (!priceCheck.valid) return priceCheck;
519
+ }
520
+ return PREFLIGHT_OK;
521
+ }
447
522
  async function payWithMpp(args) {
448
523
  const { signer, client, options } = args;
524
+ const pf = preflightPay({ url: options.url, maxPrice: options.maxPrice });
525
+ if (!pf.valid) throw new exports.T2000Error(pf.code, pf.error);
449
526
  const method = (options.method ?? "GET").toUpperCase();
450
527
  const canHaveBody = method !== "GET" && method !== "HEAD";
451
528
  const reqInit = {
@@ -525,40 +602,26 @@ async function ensureAddressBalanceCovers(args) {
525
602
  required: amountRaw.toString()
526
603
  });
527
604
  }
605
+ const coins = [];
528
606
  let coinSum = 0n;
529
607
  let cursor;
530
608
  let hasNext = true;
531
609
  while (hasNext) {
532
610
  const page = await client.core.listCoins({ owner, coinType: asset, cursor: cursor ?? void 0 });
533
- for (const c of page.objects) coinSum += BigInt(c.balance);
611
+ for (const c of page.objects) {
612
+ coins.push({ objectId: c.objectId, balance: BigInt(c.balance) });
613
+ coinSum += BigInt(c.balance);
614
+ }
534
615
  cursor = page.cursor;
535
616
  hasNext = page.hasNextPage;
536
617
  }
537
618
  const addressBalance = total - coinSum;
538
619
  if (addressBalance >= amountRaw) return 0;
539
620
  const shortfall = amountRaw - addressBalance;
540
- const { Transaction: Transaction2 } = await import('@mysten/sui/transactions');
541
- const { selectAndSplitCoin: selectAndSplitCoin2 } = await Promise.resolve().then(() => (init_coinSelection(), coinSelection_exports));
621
+ const { buildCoinToAddressBalanceMigration: buildCoinToAddressBalanceMigration2 } = await Promise.resolve().then(() => (init_coinSelection(), coinSelection_exports));
542
622
  const grpcClient = await makeGrpcBuildClient(client);
543
- const migration = await executeTx(
544
- client,
545
- signer,
546
- async () => {
547
- const tx = new Transaction2();
548
- const { coin } = await selectAndSplitCoin2(tx, client, owner, asset, shortfall, {
549
- sponsoredContext: true,
550
- // coin-objects-only — never the address balance
551
- allowSwapAll: false
552
- });
553
- tx.moveCall({
554
- target: "0x2::coin::send_funds",
555
- typeArguments: [asset],
556
- arguments: [coin, tx.pure.address(owner)]
557
- });
558
- return tx;
559
- },
560
- { buildClient: grpcClient }
561
- );
623
+ const { tx } = buildCoinToAddressBalanceMigration2({ coins, coinType: asset, owner, minAmount: shortfall });
624
+ const migration = await executeTx(client, signer, () => tx, { buildClient: grpcClient });
562
625
  return migration.gasCostSui;
563
626
  }
564
627
  async function finalize(response, opts) {
@@ -656,12 +719,35 @@ var SUPPORTED_ASSETS = {
656
719
  }
657
720
  };
658
721
  var STABLE_ASSETS = ["USDC", "USDsui"];
659
- ({
722
+ var OPERATION_ASSETS = {
723
+ send: ["USDC", "USDsui", "SUI"],
724
+ swap: "*"
725
+ };
726
+ function isAllowedAsset(op, asset) {
727
+ const allowed = OPERATION_ASSETS[op];
728
+ if (allowed === "*") return true;
729
+ const target = asset.toLowerCase();
730
+ return allowed.some((a) => a.toLowerCase() === target);
731
+ }
732
+ function assertAllowedAsset(op, asset) {
733
+ if (!asset) return;
734
+ if (!isAllowedAsset(op, asset)) {
735
+ const allowed = OPERATION_ASSETS[op];
736
+ const list = Array.isArray(allowed) ? allowed.join(", ") : "any";
737
+ const swapHint = " Swap to USDC or USDsui first, or send SUI." ;
738
+ throw new exports.T2000Error(
739
+ "INVALID_ASSET",
740
+ `${op} only supports ${list}. Cannot use ${asset}.${swapHint}`
741
+ );
742
+ }
743
+ }
744
+ var GASLESS_STABLE_TYPES = {
660
745
  USDC: SUPPORTED_ASSETS.USDC.type,
661
746
  USDsui: SUPPORTED_ASSETS.USDsui.type
662
- });
747
+ };
663
748
  var T2000_OVERLAY_FEE_WALLET = process.env.T2000_OVERLAY_FEE_WALLET ?? "0x5366efbf2b4fe5767fe2e78eb197aa5f5d138d88ac3333fbf3f80a1927da473a";
664
749
  var DEFAULT_NETWORK = "mainnet";
750
+ var GASLESS_MIN_STABLE_AMOUNT = 0.01;
665
751
  var GAS_RESERVE_MIN = 0.05;
666
752
  init_errors();
667
753
  function validateAddress(address) {
@@ -902,6 +988,9 @@ function rawToStable(raw, decimals) {
902
988
  function getDecimals(asset) {
903
989
  return SUPPORTED_ASSETS[asset].decimals;
904
990
  }
991
+ function displayToRaw(amount, decimals) {
992
+ return BigInt(Math.round(amount * 10 ** decimals));
993
+ }
905
994
  function formatUsd(amount) {
906
995
  return `$${amount.toFixed(2)}`;
907
996
  }
@@ -939,6 +1028,23 @@ function fromBase642(b64) {
939
1028
  // src/protocols/cetus-swap.ts
940
1029
  init_token_registry();
941
1030
  init_token_registry();
1031
+ function preflightSwap(input) {
1032
+ if (typeof input.from !== "string" || input.from.trim() === "") {
1033
+ return preflightFail("INVALID_ASSET", "A `from` token is required to swap");
1034
+ }
1035
+ if (typeof input.to !== "string" || input.to.trim() === "") {
1036
+ return preflightFail("INVALID_ASSET", "A `to` token is required to swap");
1037
+ }
1038
+ const amountCheck = checkPositiveAmount(input.amount, "Amount", Number.POSITIVE_INFINITY);
1039
+ if (!amountCheck.valid) return amountCheck;
1040
+ const resolvedFrom = resolveTokenType(input.from);
1041
+ const resolvedTo = resolveTokenType(input.to);
1042
+ const sameToken = input.from.trim() === input.to.trim() || resolvedFrom !== null && resolvedFrom === resolvedTo;
1043
+ if (sameToken) {
1044
+ return preflightFail("INVALID_ASSET", `Cannot swap ${input.from} to itself`);
1045
+ }
1046
+ return PREFLIGHT_OK;
1047
+ }
942
1048
  var OVERLAY_FEE_RATE = 1e-3;
943
1049
  var clientCache = /* @__PURE__ */ new Map();
944
1050
  function getClient(walletAddress, overlayFee) {
@@ -1004,6 +1110,64 @@ async function buildSwapTx(params) {
1004
1110
  });
1005
1111
  return outputCoin;
1006
1112
  }
1113
+ init_errors();
1114
+ function preflightSend(input) {
1115
+ try {
1116
+ assertAllowedAsset("send", input.asset);
1117
+ } catch (e) {
1118
+ return preflightFail("INVALID_ASSET", e.message);
1119
+ }
1120
+ const amountCheck = checkPositiveAmount(input.amount);
1121
+ if (!amountCheck.valid) return amountCheck;
1122
+ if ((input.asset === "USDC" || input.asset === "USDsui") && input.amount < GASLESS_MIN_STABLE_AMOUNT) {
1123
+ return preflightFail(
1124
+ "INVALID_AMOUNT",
1125
+ `Minimum gasless transfer is ${GASLESS_MIN_STABLE_AMOUNT} ${input.asset}. Got ${input.amount}.`
1126
+ );
1127
+ }
1128
+ const addressCheck = checkSuiAddress(input.to);
1129
+ if (!addressCheck.valid) return addressCheck;
1130
+ return PREFLIGHT_OK;
1131
+ }
1132
+ async function buildSendTx({
1133
+ client,
1134
+ address,
1135
+ to,
1136
+ amount,
1137
+ asset
1138
+ }) {
1139
+ const pf = preflightSend({ to, amount, asset });
1140
+ if (!pf.valid) throw new exports.T2000Error(pf.code, pf.error);
1141
+ const recipient = validateAddress(to);
1142
+ const assetInfo = SUPPORTED_ASSETS[asset];
1143
+ if (!assetInfo) throw new exports.T2000Error("ASSET_NOT_SUPPORTED", `Asset ${asset} is not supported`);
1144
+ const rawAmount = displayToRaw(amount, assetInfo.decimals);
1145
+ const tx = new transactions.Transaction();
1146
+ tx.setSender(address);
1147
+ const balanceResp = await client.core.getBalance({ owner: address, coinType: assetInfo.type });
1148
+ const totalBalance = BigInt(balanceResp.balance.balance);
1149
+ if (totalBalance < rawAmount) {
1150
+ throw new exports.T2000Error("INSUFFICIENT_BALANCE", `Insufficient ${asset} balance`, {
1151
+ available: Number(totalBalance) / 10 ** assetInfo.decimals,
1152
+ required: amount
1153
+ });
1154
+ }
1155
+ if (asset === "SUI") {
1156
+ const [sendCoin] = tx.splitCoins(tx.gas, [rawAmount]);
1157
+ tx.transferObjects([sendCoin], recipient);
1158
+ return tx;
1159
+ }
1160
+ const coinType = GASLESS_STABLE_TYPES[asset];
1161
+ tx.moveCall({
1162
+ target: "0x2::balance::send_funds",
1163
+ typeArguments: [coinType],
1164
+ arguments: [
1165
+ tx.balance({ type: coinType, balance: rawAmount }),
1166
+ tx.pure.address(recipient)
1167
+ ]
1168
+ });
1169
+ return tx;
1170
+ }
1007
1171
 
1008
1172
  // src/browser.ts
1009
1173
  init_token_registry();
@@ -1016,13 +1180,18 @@ exports.KeypairSigner = KeypairSigner;
1016
1180
  exports.LABEL_PATTERNS = LABEL_PATTERNS;
1017
1181
  exports.MIST_PER_SUI = MIST_PER_SUI;
1018
1182
  exports.OVERLAY_FEE_RATE = OVERLAY_FEE_RATE;
1183
+ exports.PREFLIGHT_MAX_AMOUNT = PREFLIGHT_MAX_AMOUNT;
1184
+ exports.PREFLIGHT_OK = PREFLIGHT_OK;
1019
1185
  exports.STABLE_ASSETS = STABLE_ASSETS;
1020
1186
  exports.SUI_DECIMALS = SUI_DECIMALS;
1021
1187
  exports.SUPPORTED_ASSETS = SUPPORTED_ASSETS;
1022
1188
  exports.T2000_OVERLAY_FEE_WALLET = T2000_OVERLAY_FEE_WALLET;
1023
1189
  exports.USDC_DECIMALS = USDC_DECIMALS;
1024
1190
  exports.ZkLoginSigner = ZkLoginSigner;
1191
+ exports.buildSendTx = buildSendTx;
1025
1192
  exports.buildSwapTx = buildSwapTx;
1193
+ exports.checkPositiveAmount = checkPositiveAmount;
1194
+ exports.checkSuiAddress = checkSuiAddress;
1026
1195
  exports.classifyAction = classifyAction;
1027
1196
  exports.classifyLabel = classifyLabel;
1028
1197
  exports.classifyTransaction = classifyTransaction;
@@ -1044,6 +1213,10 @@ exports.mapWalletError = mapWalletError;
1044
1213
  exports.mistToSui = mistToSui;
1045
1214
  exports.parseSuiRpcTx = parseSuiRpcTx;
1046
1215
  exports.payWithMpp = payWithMpp;
1216
+ exports.preflightFail = preflightFail;
1217
+ exports.preflightPay = preflightPay;
1218
+ exports.preflightSend = preflightSend;
1219
+ exports.preflightSwap = preflightSwap;
1047
1220
  exports.rawToStable = rawToStable;
1048
1221
  exports.rawToUsdc = rawToUsdc;
1049
1222
  exports.refineLendingLabel = refineLendingLabel;