@t2000/sdk 5.0.0 → 5.2.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
@@ -5,7 +5,7 @@ import { EventEmitter } from 'eventemitter3';
5
5
  import { createPaymentTransactionUri } from '@mysten/payment-kit';
6
6
  import { SuiGrpcClient } from '@mysten/sui/grpc';
7
7
  import { SuiGraphQLClient } from '@mysten/sui/graphql';
8
- import { normalizeSuiAddress, isValidSuiAddress } from '@mysten/sui/utils';
8
+ import { normalizeSuiAddress, isValidSuiAddress, fromBase64 } from '@mysten/sui/utils';
9
9
  import { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519';
10
10
  import { decodeSuiPrivateKey } from '@mysten/sui/cryptography';
11
11
  import { access, mkdir, writeFile, readFile } from 'fs/promises';
@@ -570,8 +570,8 @@ async function addSwapToTx(tx, client, address, input) {
570
570
  inputCoin = result.coin;
571
571
  effectiveRaw = result.effectiveAmount;
572
572
  } else {
573
- const { selectAndSplitCoin: selectAndSplitCoin3 } = await Promise.resolve().then(() => (init_coinSelection(), coinSelection_exports));
574
- const result = await selectAndSplitCoin3(tx, client, address, fromType, requestedRaw, {
573
+ const { selectAndSplitCoin: selectAndSplitCoin2 } = await Promise.resolve().then(() => (init_coinSelection(), coinSelection_exports));
574
+ const result = await selectAndSplitCoin2(tx, client, address, fromType, requestedRaw, {
575
575
  sponsoredContext: input.sponsoredContext ?? false,
576
576
  mergeCache: input.coinMergeCache
577
577
  });
@@ -991,55 +991,126 @@ async function executeTx(client, signer, buildTx, options = {}) {
991
991
  return { digest: txn.digest, gasCostSui, effects };
992
992
  }
993
993
 
994
- // src/mpp-cost.ts
995
- function parseChallengeAmount(challenge) {
996
- const raw = challenge?.request?.amount;
997
- const n = typeof raw === "string" ? Number(raw) : typeof raw === "number" ? raw : Number.NaN;
998
- return Number.isFinite(n) ? n : void 0;
999
- }
1000
-
1001
994
  // src/wallet/pay.ts
995
+ init_errors();
1002
996
  async function payWithMpp(args) {
1003
997
  const { signer, client, options } = args;
1004
- const { Mppx } = await import('mppx/client');
1005
- const { sui, USDC } = await import('@suimpp/mpp/client');
1006
- const { SuiGrpcClient: SuiGrpcClient2 } = await import('@mysten/sui/grpc');
1007
- const signerAddress = signer.getAddress();
1008
- let paymentDigest;
1009
- let gasCostSui = 0;
1010
- let chargedAmount;
1011
- const network = client.network === "testnet" ? "testnet" : "mainnet";
1012
- const grpcBaseUrl = network === "testnet" ? "https://fullnode.testnet.sui.io" : "https://fullnode.mainnet.sui.io";
1013
- const grpcClient = new SuiGrpcClient2({ baseUrl: grpcBaseUrl, network });
1014
- const mppx = Mppx.create({
1015
- polyfill: false,
1016
- onChallenge: async (challenge) => {
1017
- const parsed = parseChallengeAmount(challenge);
1018
- if (parsed !== void 0) chargedAmount = parsed;
1019
- return void 0;
1020
- },
1021
- methods: [sui({
1022
- client,
1023
- currency: USDC,
1024
- signer: {
1025
- toSuiAddress: () => signerAddress,
1026
- signPersonalMessage: (bytes) => signer.signPersonalMessage(bytes)
1027
- },
1028
- execute: async (tx) => {
1029
- const result = await executeTx(client, signer, () => tx, { buildClient: grpcClient });
1030
- paymentDigest = result.digest;
1031
- gasCostSui = result.gasCostSui;
1032
- return { digest: result.digest };
1033
- }
1034
- })]
1035
- });
1036
998
  const method = (options.method ?? "GET").toUpperCase();
1037
999
  const canHaveBody = method !== "GET" && method !== "HEAD";
1038
- const response = await mppx.fetch(options.url, {
1000
+ const reqInit = {
1039
1001
  method,
1040
1002
  headers: options.headers,
1041
1003
  body: canHaveBody ? options.body : void 0
1004
+ };
1005
+ const probe = await fetch(options.url, reqInit);
1006
+ if (probe.status !== 402) {
1007
+ return finalize(probe, { paid: false });
1008
+ }
1009
+ const requirements = await pickSuiExactRequirements(probe, client.network);
1010
+ if (!requirements) {
1011
+ throw new T2000Error(
1012
+ "FACILITATOR_REJECTION",
1013
+ `Endpoint returned 402 without an x402 'exact' / sui:${client.network} payment requirement. This SDK only speaks the x402 dialect.`
1014
+ );
1015
+ }
1016
+ return payViaX402({ signer, client, options, reqInit, requirements });
1017
+ }
1018
+ async function pickSuiExactRequirements(response, network) {
1019
+ try {
1020
+ const body = await response.clone().json();
1021
+ const want = `sui:${network === "testnet" ? "testnet" : "mainnet"}`;
1022
+ return body.accepts?.find((a) => a.scheme === "exact" && a.network === want);
1023
+ } catch {
1024
+ return void 0;
1025
+ }
1026
+ }
1027
+ async function payViaX402(args) {
1028
+ const { signer, client, options, reqInit, requirements } = args;
1029
+ const { buildX402SignedPayment, X402_PAYMENT_HEADER, X402_PAYMENT_RESPONSE_HEADER } = await import('@suimpp/mpp/x402');
1030
+ const amountRaw = BigInt(requirements.maxAmountRequired);
1031
+ const migrationGasSui = await ensureAddressBalanceCovers({
1032
+ signer,
1033
+ client,
1034
+ asset: requirements.asset,
1035
+ amountRaw
1036
+ });
1037
+ const signerAdapter = {
1038
+ toSuiAddress: () => signer.getAddress(),
1039
+ signTransaction: (bytes) => signer.signTransaction(bytes)
1040
+ };
1041
+ const { header } = await buildX402SignedPayment({ requirements, signer: signerAdapter });
1042
+ const res = await fetch(options.url, {
1043
+ ...reqInit,
1044
+ headers: { ...options.headers ?? {}, [X402_PAYMENT_HEADER]: header }
1042
1045
  });
1046
+ const settleHeader = res.headers.get(X402_PAYMENT_RESPONSE_HEADER);
1047
+ const paid = !!settleHeader;
1048
+ let digest;
1049
+ if (settleHeader) {
1050
+ try {
1051
+ digest = JSON.parse(new TextDecoder().decode(fromBase64(settleHeader))).transaction;
1052
+ } catch {
1053
+ digest = void 0;
1054
+ }
1055
+ }
1056
+ const result = await finalize(res, { paid });
1057
+ if (!paid) return { ...result, dialect: "x402" };
1058
+ return {
1059
+ ...result,
1060
+ dialect: "x402",
1061
+ cost: atomicToHuman(amountRaw, await assetDecimals(requirements.asset)),
1062
+ gasCostSui: migrationGasSui,
1063
+ receipt: digest ? { reference: digest, timestamp: (/* @__PURE__ */ new Date()).toISOString() } : result.receipt
1064
+ };
1065
+ }
1066
+ async function ensureAddressBalanceCovers(args) {
1067
+ const { signer, client, asset, amountRaw } = args;
1068
+ const owner = signer.getAddress();
1069
+ const balanceResp = await client.core.getBalance({ owner, coinType: asset });
1070
+ const total = BigInt(balanceResp.balance.balance);
1071
+ if (total < amountRaw) {
1072
+ throw new T2000Error("INSUFFICIENT_BALANCE", `Insufficient ${asset} to pay`, {
1073
+ available: total.toString(),
1074
+ required: amountRaw.toString()
1075
+ });
1076
+ }
1077
+ let coinSum = 0n;
1078
+ let cursor;
1079
+ let hasNext = true;
1080
+ while (hasNext) {
1081
+ const page = await client.core.listCoins({ owner, coinType: asset, cursor: cursor ?? void 0 });
1082
+ for (const c of page.objects) coinSum += BigInt(c.balance);
1083
+ cursor = page.cursor;
1084
+ hasNext = page.hasNextPage;
1085
+ }
1086
+ const addressBalance = total - coinSum;
1087
+ if (addressBalance >= amountRaw) return 0;
1088
+ const shortfall = amountRaw - addressBalance;
1089
+ const { Transaction: Transaction6 } = await import('@mysten/sui/transactions');
1090
+ const { selectAndSplitCoin: selectAndSplitCoin2 } = await Promise.resolve().then(() => (init_coinSelection(), coinSelection_exports));
1091
+ const grpcClient = await makeGrpcBuildClient(client);
1092
+ const migration = await executeTx(
1093
+ client,
1094
+ signer,
1095
+ async () => {
1096
+ const tx = new Transaction6();
1097
+ const { coin } = await selectAndSplitCoin2(tx, client, owner, asset, shortfall, {
1098
+ sponsoredContext: true,
1099
+ // coin-objects-only — never the address balance
1100
+ allowSwapAll: false
1101
+ });
1102
+ tx.moveCall({
1103
+ target: "0x2::coin::send_funds",
1104
+ typeArguments: [asset],
1105
+ arguments: [coin, tx.pure.address(owner)]
1106
+ });
1107
+ return tx;
1108
+ },
1109
+ { buildClient: grpcClient }
1110
+ );
1111
+ return migration.gasCostSui;
1112
+ }
1113
+ async function finalize(response, opts) {
1043
1114
  const contentType = response.headers.get("content-type") ?? "";
1044
1115
  let body;
1045
1116
  try {
@@ -1047,15 +1118,25 @@ async function payWithMpp(args) {
1047
1118
  } catch {
1048
1119
  body = null;
1049
1120
  }
1050
- const paid = !!paymentDigest;
1051
- return {
1052
- status: response.status,
1053
- body,
1054
- paid,
1055
- cost: paid ? chargedAmount ?? options.maxPrice ?? void 0 : void 0,
1056
- gasCostSui: paid ? gasCostSui : void 0,
1057
- receipt: paymentDigest ? { reference: paymentDigest, timestamp: (/* @__PURE__ */ new Date()).toISOString() } : void 0
1058
- };
1121
+ return { status: response.status, body, paid: opts.paid };
1122
+ }
1123
+ async function makeGrpcBuildClient(client) {
1124
+ const { SuiGrpcClient: SuiGrpcClient2 } = await import('@mysten/sui/grpc');
1125
+ const network = client.network === "testnet" ? "testnet" : "mainnet";
1126
+ const baseUrl = network === "testnet" ? "https://fullnode.testnet.sui.io" : "https://fullnode.mainnet.sui.io";
1127
+ return new SuiGrpcClient2({ baseUrl, network });
1128
+ }
1129
+ function atomicToHuman(raw, decimals) {
1130
+ return Number(raw) / 10 ** decimals;
1131
+ }
1132
+ async function assetDecimals(coinType) {
1133
+ try {
1134
+ const { getDecimalsForCoinType: getDecimalsForCoinType2 } = await Promise.resolve().then(() => (init_token_registry(), token_registry_exports));
1135
+ const d = getDecimalsForCoinType2(coinType);
1136
+ return typeof d === "number" ? d : 6;
1137
+ } catch {
1138
+ return 6;
1139
+ }
1059
1140
  }
1060
1141
 
1061
1142
  // src/wallet/zkLoginSigner.ts
@@ -1678,170 +1759,198 @@ async function normalizeAddressInput(value, ctx = {}) {
1678
1759
 
1679
1760
  // src/t2000.ts
1680
1761
  init_errors();
1762
+ var DEFAULT_CONFIG_DIR = join(homedir(), ".t2000");
1763
+ function resolveConfigPath(configDir) {
1764
+ return join(configDir ?? DEFAULT_CONFIG_DIR, "config.json");
1765
+ }
1766
+ function todayUtc() {
1767
+ return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1768
+ }
1769
+ function readLimitsFile(configDir) {
1770
+ const path = resolveConfigPath(configDir);
1771
+ try {
1772
+ return sanitize(JSON.parse(readFileSync(path, "utf-8")));
1773
+ } catch {
1774
+ return {};
1775
+ }
1776
+ }
1777
+ function writeLimitsFile(file, configDir) {
1778
+ const path = resolveConfigPath(configDir);
1779
+ let existing = {};
1780
+ try {
1781
+ existing = JSON.parse(readFileSync(path, "utf-8"));
1782
+ } catch {
1783
+ existing = {};
1784
+ }
1785
+ for (const dead of ["maxPerTx", "maxDailySend", "dailyUsed", "dailyResetDate", "locked"]) {
1786
+ delete existing[dead];
1787
+ }
1788
+ const merged = { ...existing };
1789
+ if (file.limits && (file.limits.perTxUsd !== void 0 || file.limits.dailyUsd !== void 0)) {
1790
+ merged.limits = file.limits;
1791
+ } else {
1792
+ delete merged.limits;
1793
+ }
1794
+ if (file.dailySpend) merged.dailySpend = file.dailySpend;
1795
+ else delete merged.dailySpend;
1796
+ const dir = dirname(path);
1797
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
1798
+ writeFileSync(path, JSON.stringify(merged, null, 2) + "\n", { mode: 384 });
1799
+ return path;
1800
+ }
1801
+ function getLimits(configDir) {
1802
+ return readLimitsFile(configDir).limits;
1803
+ }
1804
+ function hasLimits(configDir) {
1805
+ const l = readLimitsFile(configDir).limits;
1806
+ return !!l && (l.perTxUsd !== void 0 || l.dailyUsd !== void 0);
1807
+ }
1808
+ function setLimits(limits, configDir) {
1809
+ const file = readLimitsFile(configDir);
1810
+ const merged = { ...file.limits };
1811
+ if (limits.perTxUsd !== void 0) merged.perTxUsd = limits.perTxUsd;
1812
+ if (limits.dailyUsd !== void 0) merged.dailyUsd = limits.dailyUsd;
1813
+ return writeLimitsFile({ ...file, limits: merged }, configDir);
1814
+ }
1815
+ function clearLimits(configDir) {
1816
+ const file = readLimitsFile(configDir);
1817
+ return writeLimitsFile({ ...file, limits: void 0 }, configDir);
1818
+ }
1819
+ function dailySpentToday(configDir) {
1820
+ const { dailySpend } = readLimitsFile(configDir);
1821
+ if (!dailySpend || dailySpend.date !== todayUtc()) return 0;
1822
+ return dailySpend.usd;
1823
+ }
1824
+ function recordDailySpend(usd, configDir) {
1825
+ if (!Number.isFinite(usd) || usd <= 0) return;
1826
+ const file = readLimitsFile(configDir);
1827
+ const today = todayUtc();
1828
+ const prior = file.dailySpend && file.dailySpend.date === today ? file.dailySpend.usd : 0;
1829
+ writeLimitsFile({ ...file, dailySpend: { date: today, usd: prior + usd } }, configDir);
1830
+ }
1831
+ function sanitize(raw) {
1832
+ if (typeof raw !== "object" || raw === null) return {};
1833
+ const r = raw;
1834
+ const out = {};
1835
+ if (typeof r.limits === "object" && r.limits !== null) {
1836
+ const l = r.limits;
1837
+ const limits = {};
1838
+ if (typeof l.perTxUsd === "number" && l.perTxUsd > 0) limits.perTxUsd = l.perTxUsd;
1839
+ if (typeof l.dailyUsd === "number" && l.dailyUsd > 0) limits.dailyUsd = l.dailyUsd;
1840
+ else if (typeof l.dailySendUsd === "number" && l.dailySendUsd > 0) limits.dailyUsd = l.dailySendUsd;
1841
+ if (limits.perTxUsd !== void 0 || limits.dailyUsd !== void 0) out.limits = limits;
1842
+ }
1843
+ if (typeof r.dailySpend === "object" && r.dailySpend !== null) {
1844
+ const d = r.dailySpend;
1845
+ if (typeof d.date === "string" && typeof d.usd === "number" && d.usd >= 0) {
1846
+ out.dailySpend = { date: d.date, usd: d.usd };
1847
+ }
1848
+ }
1849
+ return out;
1850
+ }
1681
1851
 
1682
- // src/safeguards/types.ts
1683
- var DEFAULT_SAFEGUARD_CONFIG = {
1684
- locked: false,
1685
- maxPerTx: 0,
1686
- maxDailySend: 0,
1687
- dailyUsed: 0,
1688
- dailyResetDate: ""
1689
- };
1690
-
1691
- // src/safeguards/errors.ts
1692
- init_errors();
1693
- var SafeguardError = class extends T2000Error {
1694
- rule;
1695
- details;
1696
- constructor(rule, details, message) {
1697
- const msg = message ?? buildMessage(rule, details);
1698
- super("SAFEGUARD_BLOCKED", msg, { rule, ...details });
1699
- this.name = "SafeguardError";
1700
- this.rule = rule;
1701
- this.details = details;
1852
+ // src/limits/errors.ts
1853
+ var LimitExceededError = class extends Error {
1854
+ code = "LIMIT_EXCEEDED";
1855
+ operation;
1856
+ limitKind;
1857
+ limit;
1858
+ attempted;
1859
+ constructor(params) {
1860
+ const label = params.limitKind === "perTxUsd" ? "per-transaction limit" : "daily spend limit";
1861
+ super(
1862
+ `Exceeds ${label} ($${params.limit}). Attempted $${params.attempted.toFixed(2)}. Use --force / force:true to override.`
1863
+ );
1864
+ this.name = "LimitExceededError";
1865
+ this.operation = params.operation;
1866
+ this.limitKind = params.limitKind;
1867
+ this.limit = params.limit;
1868
+ this.attempted = params.attempted;
1702
1869
  }
1703
1870
  toJSON() {
1704
1871
  return {
1705
- error: "SAFEGUARD_BLOCKED",
1872
+ error: this.code,
1706
1873
  message: this.message,
1707
- retryable: this.retryable,
1708
- data: { rule: this.rule, ...this.details }
1874
+ operation: this.operation,
1875
+ limitKind: this.limitKind,
1876
+ limit: this.limit,
1877
+ attempted: this.attempted
1709
1878
  };
1710
1879
  }
1711
1880
  };
1712
- function buildMessage(rule, details) {
1713
- switch (rule) {
1714
- case "locked":
1715
- return "Agent is locked. All operations are frozen.";
1716
- case "maxPerTx":
1717
- return `Amount $${(details.attempted ?? 0).toFixed(2)} exceeds per-transaction limit ($${(details.limit ?? 0).toFixed(2)})`;
1718
- case "maxDailySend":
1719
- return `Daily send limit reached ($${(details.current ?? 0).toFixed(2)}/$${(details.limit ?? 0).toFixed(2)} used today)`;
1881
+
1882
+ // src/limits/enforce.ts
1883
+ function approxUsdValue(asset, amount) {
1884
+ const symbol = asset.toUpperCase();
1885
+ if (symbol === "USDC" || symbol === "USDSUI") return amount;
1886
+ return null;
1887
+ }
1888
+ function assertLimitConfig(input) {
1889
+ if (input.force) return;
1890
+ if (!Number.isFinite(input.amountUsd) || input.amountUsd <= 0) return;
1891
+ const { limits } = input;
1892
+ if (!limits) return;
1893
+ if (limits.perTxUsd !== void 0 && input.amountUsd > limits.perTxUsd) {
1894
+ throw new LimitExceededError({
1895
+ operation: input.operation,
1896
+ limitKind: "perTxUsd",
1897
+ limit: limits.perTxUsd,
1898
+ attempted: input.amountUsd
1899
+ });
1900
+ }
1901
+ if (limits.dailyUsd !== void 0 && input.spentTodayUsd + input.amountUsd > limits.dailyUsd) {
1902
+ throw new LimitExceededError({
1903
+ operation: input.operation,
1904
+ limitKind: "dailyUsd",
1905
+ // The cap is total/day; report the cap (attempted is THIS write's USD).
1906
+ limit: limits.dailyUsd,
1907
+ attempted: input.spentTodayUsd + input.amountUsd
1908
+ });
1720
1909
  }
1721
1910
  }
1722
-
1723
- // src/safeguards/enforcer.ts
1724
- var SafeguardEnforcer = class {
1725
- config;
1726
- configPath;
1911
+ var LimitEnforcer = class {
1727
1912
  constructor(configDir) {
1728
- this.config = { ...DEFAULT_SAFEGUARD_CONFIG };
1729
- this.configPath = configDir ? join(configDir, "config.json") : null;
1913
+ this.configDir = configDir;
1914
+ }
1915
+ /** Throws `LimitExceededError` when the write exceeds an opted-in cap. */
1916
+ assert(input) {
1917
+ assertLimitConfig({
1918
+ limits: getLimits(this.configDir),
1919
+ spentTodayUsd: dailySpentToday(this.configDir),
1920
+ operation: input.operation,
1921
+ amountUsd: input.amountUsd,
1922
+ force: input.force
1923
+ });
1730
1924
  }
1731
- load() {
1732
- if (!this.configPath) return;
1733
- try {
1734
- const raw = JSON.parse(readFileSync(this.configPath, "utf-8"));
1735
- this.config = {
1736
- ...DEFAULT_SAFEGUARD_CONFIG,
1737
- locked: raw.locked ?? false,
1738
- maxPerTx: raw.maxPerTx ?? 0,
1739
- maxDailySend: raw.maxDailySend ?? 0,
1740
- dailyUsed: raw.dailyUsed ?? 0,
1741
- dailyResetDate: raw.dailyResetDate ?? ""
1742
- };
1743
- } catch {
1744
- this.config = { ...DEFAULT_SAFEGUARD_CONFIG };
1745
- }
1925
+ /** Add a settled write's USD value to today's cumulative total. */
1926
+ record(amountUsd) {
1927
+ recordDailySpend(amountUsd, this.configDir);
1746
1928
  }
1747
- assertNotLocked() {
1748
- this.load();
1749
- if (this.config.locked) {
1750
- throw new SafeguardError("locked", {});
1751
- }
1929
+ getLimits() {
1930
+ return getLimits(this.configDir);
1752
1931
  }
1753
- check(metadata) {
1754
- this.load();
1755
- if (this.config.locked) {
1756
- throw new SafeguardError("locked", {});
1757
- }
1758
- const amount = metadata.amount ?? 0;
1759
- if (this.config.maxPerTx > 0 && amount > this.config.maxPerTx) {
1760
- throw new SafeguardError("maxPerTx", {
1761
- attempted: amount,
1762
- limit: this.config.maxPerTx
1763
- });
1764
- }
1765
- this.resetDailyIfNewDay();
1766
- if (this.config.maxDailySend > 0 && this.config.dailyUsed + amount > this.config.maxDailySend) {
1767
- throw new SafeguardError("maxDailySend", {
1768
- attempted: amount,
1769
- limit: this.config.maxDailySend,
1770
- current: this.config.dailyUsed
1771
- });
1772
- }
1932
+ hasLimits() {
1933
+ return hasLimits(this.configDir);
1773
1934
  }
1774
- recordUsage(amount) {
1775
- this.resetDailyIfNewDay();
1776
- this.config.dailyUsed += amount;
1777
- this.save();
1778
- }
1779
- lock() {
1780
- this.config.locked = true;
1781
- this.save();
1782
- }
1783
- unlock() {
1784
- this.config.locked = false;
1785
- this.save();
1786
- }
1787
- set(key, value) {
1788
- if (key === "locked" && typeof value === "boolean") {
1789
- this.config.locked = value;
1790
- } else if (key === "maxPerTx" && typeof value === "number") {
1791
- this.config.maxPerTx = value;
1792
- } else if (key === "maxDailySend" && typeof value === "number") {
1793
- this.config.maxDailySend = value;
1794
- }
1795
- this.save();
1796
- }
1797
- getConfig() {
1798
- this.load();
1799
- this.resetDailyIfNewDay();
1800
- return { ...this.config };
1801
- }
1802
- isConfigured() {
1803
- return this.config.maxPerTx > 0 || this.config.maxDailySend > 0;
1804
- }
1805
- resetDailyIfNewDay() {
1806
- const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1807
- if (this.config.dailyResetDate !== today) {
1808
- this.config.dailyUsed = 0;
1809
- this.config.dailyResetDate = today;
1810
- this.save();
1811
- }
1935
+ setLimits(limits) {
1936
+ setLimits(limits, this.configDir);
1812
1937
  }
1813
- save() {
1814
- if (!this.configPath) return;
1815
- try {
1816
- let existing = {};
1817
- try {
1818
- existing = JSON.parse(readFileSync(this.configPath, "utf-8"));
1819
- } catch {
1820
- }
1821
- const merged = {
1822
- ...existing,
1823
- locked: this.config.locked,
1824
- maxPerTx: this.config.maxPerTx,
1825
- maxDailySend: this.config.maxDailySend,
1826
- dailyUsed: this.config.dailyUsed,
1827
- dailyResetDate: this.config.dailyResetDate
1828
- };
1829
- const dir = this.configPath.replace(/[/\\][^/\\]+$/, "");
1830
- if (!existsSync(dir)) {
1831
- mkdirSync(dir, { recursive: true });
1832
- }
1833
- writeFileSync(this.configPath, JSON.stringify(merged, null, 2) + "\n");
1834
- } catch {
1835
- }
1938
+ clearLimits() {
1939
+ clearLimits(this.configDir);
1940
+ }
1941
+ dailySpentToday() {
1942
+ return dailySpentToday(this.configDir);
1836
1943
  }
1837
1944
  };
1838
- var DEFAULT_CONFIG_DIR = join(homedir(), ".t2000");
1945
+ var DEFAULT_CONFIG_DIR2 = join(homedir(), ".t2000");
1839
1946
  var T2000 = class _T2000 extends EventEmitter {
1840
1947
  _signer;
1841
1948
  _keypair;
1842
1949
  client;
1843
1950
  _address;
1844
- enforcer;
1951
+ /** Unified spending-limit gate (per-tx + cumulative daily, USD). Shared by
1952
+ * CLI + MCP + programmatic writes — one gate, no bypass (R-0 Finding 1). */
1953
+ limits;
1845
1954
  constructor(keypairOrSigner, client, configDir, isSignerMode) {
1846
1955
  super();
1847
1956
  if (isSignerMode) {
@@ -1855,8 +1964,7 @@ var T2000 = class _T2000 extends EventEmitter {
1855
1964
  this._address = getAddress(kp);
1856
1965
  }
1857
1966
  this.client = client;
1858
- this.enforcer = new SafeguardEnforcer(configDir);
1859
- this.enforcer.load();
1967
+ this.limits = new LimitEnforcer(configDir);
1860
1968
  }
1861
1969
  static async create(options = {}) {
1862
1970
  const { keyPath, rpcUrl } = options;
@@ -1869,7 +1977,7 @@ var T2000 = class _T2000 extends EventEmitter {
1869
1977
  );
1870
1978
  }
1871
1979
  const keypair = await loadKey(void 0, keyPath);
1872
- return new _T2000(keypair, client, DEFAULT_CONFIG_DIR);
1980
+ return new _T2000(keypair, client, DEFAULT_CONFIG_DIR2);
1873
1981
  }
1874
1982
  static fromPrivateKey(privateKey, options = {}) {
1875
1983
  const keypair = keypairFromPrivateKey(privateKey);
@@ -1880,7 +1988,7 @@ var T2000 = class _T2000 extends EventEmitter {
1880
1988
  const keypair = generateKeypair();
1881
1989
  await saveKey(keypair, void 0, options.keyPath);
1882
1990
  const client = getSuiClient();
1883
- const agent = new _T2000(keypair, client, DEFAULT_CONFIG_DIR);
1991
+ const agent = new _T2000(keypair, client, DEFAULT_CONFIG_DIR2);
1884
1992
  const address = agent.address();
1885
1993
  return { agent, address };
1886
1994
  }
@@ -1902,11 +2010,10 @@ var T2000 = class _T2000 extends EventEmitter {
1902
2010
  }
1903
2011
  // -- MPP Payments --
1904
2012
  async pay(options) {
1905
- this.enforcer.assertNotLocked();
1906
- this.enforcer.check({ operation: "pay", amount: options.maxPrice ?? 1 });
2013
+ this.limits.assert({ operation: "pay", amountUsd: options.maxPrice ?? 0, force: options.force });
1907
2014
  const result = await payWithMpp({ signer: this._signer, client: this.client, options });
1908
2015
  if (result.paid) {
1909
- this.enforcer.recordUsage(result.cost ?? options.maxPrice ?? 1);
2016
+ this.limits.record(result.cost ?? options.maxPrice ?? 0);
1910
2017
  }
1911
2018
  return result;
1912
2019
  }
@@ -1920,7 +2027,11 @@ var T2000 = class _T2000 extends EventEmitter {
1920
2027
  // and the S.323 build-tracker entry.
1921
2028
  // -- Swap --
1922
2029
  async swap(params) {
1923
- this.enforcer.assertNotLocked();
2030
+ this.limits.assert({
2031
+ operation: "swap",
2032
+ amountUsd: approxUsdValue(params.from, params.amount) ?? 0,
2033
+ force: params.force
2034
+ });
1924
2035
  const { findSwapRoute: findSwapRoute2, buildSwapTx: buildSwapTx2, resolveTokenType: resolveTokenType2 } = await Promise.resolve().then(() => (init_cetus_swap(), cetus_swap_exports));
1925
2036
  const fromType = resolveTokenType2(params.from);
1926
2037
  const toType = resolveTokenType2(params.to);
@@ -2012,6 +2123,7 @@ var T2000 = class _T2000 extends EventEmitter {
2012
2123
  const fromName = resolveSymbol(fromType);
2013
2124
  const toName = resolveSymbol(toType);
2014
2125
  const routeDesc = route.routerData.paths?.map((p) => p.provider).filter(Boolean).slice(0, 3).join(" + ") ?? "Cetus Aggregator";
2126
+ this.limits.record(approxUsdValue(params.from, params.amount) ?? 0);
2015
2127
  return {
2016
2128
  success: true,
2017
2129
  tx: gasResult.digest,
@@ -2068,7 +2180,6 @@ var T2000 = class _T2000 extends EventEmitter {
2068
2180
  * the "build via gRPC, execute via JSON-RPC" hybrid).
2069
2181
  */
2070
2182
  async send(params) {
2071
- this.enforcer.assertNotLocked();
2072
2183
  const asset = params.asset;
2073
2184
  if (!asset) {
2074
2185
  throw new T2000Error(
@@ -2078,6 +2189,11 @@ var T2000 = class _T2000 extends EventEmitter {
2078
2189
  }
2079
2190
  assertAllowedAsset("send", asset);
2080
2191
  const sendableAsset = asset;
2192
+ this.limits.assert({
2193
+ operation: "send",
2194
+ amountUsd: approxUsdValue(sendableAsset, params.amount) ?? 0,
2195
+ force: params.force
2196
+ });
2081
2197
  const resolved = await this.resolveRecipient(params.to);
2082
2198
  const sendAmount = params.amount;
2083
2199
  const sendTo = resolved.address;
@@ -2089,7 +2205,7 @@ var T2000 = class _T2000 extends EventEmitter {
2089
2205
  () => buildSendTx({ client: this.client, address: this._address, to: sendTo, amount: sendAmount, asset: sendableAsset }),
2090
2206
  { buildClient }
2091
2207
  );
2092
- this.enforcer.recordUsage(sendAmount);
2208
+ this.limits.record(approxUsdValue(sendableAsset, sendAmount) ?? 0);
2093
2209
  const balance = await this.balance();
2094
2210
  this.emitBalanceChange(sendableAsset, sendAmount, "send", gasResult.digest);
2095
2211
  return {
@@ -2641,6 +2757,6 @@ function displayHandle(label) {
2641
2757
  return `${label}@${parentDisplay}`;
2642
2758
  }
2643
2759
 
2644
- export { AUDRIC_PARENT_NAME, AUDRIC_PARENT_NFT_ID, CETUS_USDC_SUI_POOL, CLOCK_ID, COIN_REGISTRY, DEFAULT_GRPC_URL, DEFAULT_NETWORK, DEFAULT_SAFEGUARD_CONFIG, ETH_TYPE, GASLESS_MIN_STABLE_AMOUNT, GASLESS_STABLE_TYPES, GAS_RESERVE_MIN, IKA_TYPE, InvalidAddressError, KNOWN_TARGETS, KeypairSigner, LABEL_PATTERNS, LOFI_TYPE, MANIFEST_TYPE, MIST_PER_SUI, NAVX_TYPE, OPERATION_ASSETS, OVERLAY_FEE_RATE, 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, addSendToTx, addSwapToTx, assertAllowedAsset, buildAddLeafTx, buildRevokeLeafTx, buildSendTx, buildSwapTx, classifyAction, classifyLabel, classifyTransaction, composeTx, deriveAllowedAddressesFromPtb, deserializeCetusRoute, displayHandle, executeTx, exportPrivateKey, extractAllUserLegs, extractTransferDetails, extractTxCommands, extractTxSender, fallbackLabel, fetchAllCoins, findSwapRoute, formatAssetAmount, formatSui, formatUsd, fullHandle, generateKeypair, getAddress, getCoinMeta, getDecimals, getDecimalsForCoinType, getSponsoredSwapProviders, getSuiClient, getSuiGrpcClient, getSwapQuote, isAllowedAsset, isCetusRouteFresh, isInRegistry, keypairFromPrivateKey, loadKey, looksLikeSuiNs, mapMoveAbortCode, mapWalletError, mistToSui, normalizeAddressInput, normalizeAsset, normalizeCoinType, parseSuiRpcTx, payWithMpp, queryHistory, queryTransaction, rawToStable, rawToUsdc, refineLendingLabel, resolveAddressToSuinsViaRpc, resolveSuinsViaRpc, resolveSymbol, resolveTokenType, saveBech32, saveKey, selectAndSplitCoin, selectSuiCoin, serializeCetusRoute, simulateTransaction, stableToRaw, suiToMist, throwIfSimulationFailed, truncateAddress, usdcToRaw, validateAddress, validateLabel, verifyCetusRouteCoinMatch, walletExists };
2760
+ export { AUDRIC_PARENT_NAME, AUDRIC_PARENT_NFT_ID, CETUS_USDC_SUI_POOL, CLOCK_ID, COIN_REGISTRY, DEFAULT_GRPC_URL, DEFAULT_NETWORK, ETH_TYPE, GASLESS_MIN_STABLE_AMOUNT, GASLESS_STABLE_TYPES, GAS_RESERVE_MIN, IKA_TYPE, InvalidAddressError, KNOWN_TARGETS, KeypairSigner, LABEL_PATTERNS, LOFI_TYPE, LimitEnforcer, LimitExceededError, MANIFEST_TYPE, MIST_PER_SUI, NAVX_TYPE, OPERATION_ASSETS, OVERLAY_FEE_RATE, SENDABLE_ASSETS, SPONSORED_PYTH_DEPENDENT_PROVIDERS, STABLE_ASSETS, SUINS_NAME_REGEX, SUI_ADDRESS_REGEX, SUI_ADDRESS_STRICT_REGEX, SUI_DECIMALS, SUI_TYPE, SUPPORTED_ASSETS, 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, addSendToTx, addSwapToTx, approxUsdValue, assertAllowedAsset, assertLimitConfig, buildAddLeafTx, buildRevokeLeafTx, buildSendTx, buildSwapTx, classifyAction, classifyLabel, classifyTransaction, clearLimits, composeTx, dailySpentToday, deriveAllowedAddressesFromPtb, deserializeCetusRoute, displayHandle, executeTx, exportPrivateKey, extractAllUserLegs, extractTransferDetails, extractTxCommands, extractTxSender, fallbackLabel, fetchAllCoins, findSwapRoute, formatAssetAmount, formatSui, formatUsd, fullHandle, generateKeypair, getAddress, getCoinMeta, getDecimals, getDecimalsForCoinType, getLimits, getSponsoredSwapProviders, getSuiClient, getSuiGrpcClient, getSwapQuote, hasLimits, isAllowedAsset, isCetusRouteFresh, isInRegistry, keypairFromPrivateKey, loadKey, looksLikeSuiNs, mapMoveAbortCode, mapWalletError, mistToSui, normalizeAddressInput, normalizeAsset, normalizeCoinType, parseSuiRpcTx, payWithMpp, queryHistory, queryTransaction, rawToStable, rawToUsdc, readLimitsFile, recordDailySpend, refineLendingLabel, resolveAddressToSuinsViaRpc, resolveSuinsViaRpc, resolveSymbol, resolveTokenType, saveBech32, saveKey, selectAndSplitCoin, selectSuiCoin, serializeCetusRoute, setLimits, simulateTransaction, stableToRaw, suiToMist, throwIfSimulationFailed, truncateAddress, usdcToRaw, validateAddress, validateLabel, verifyCetusRouteCoinMatch, walletExists, writeLimitsFile };
2645
2761
  //# sourceMappingURL=index.js.map
2646
2762
  //# sourceMappingURL=index.js.map