@trustwallet/cli 0.24.2 → 0.25.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
@@ -3437,24 +3437,42 @@ async function resolveDecimals(search, chain2, params) {
3437
3437
  }
3438
3438
  const isNative = params.isNative ?? address.toLowerCase() === getNativeAddressPlaceholder(chain2).toLowerCase();
3439
3439
  const known = lookupKnownTokenDecimals(chain2, isNative ? void 0 : address);
3440
- const isRawAddress = carriesRawEvmAddress(token);
3440
+ const needsDecimals = known === void 0 && !isNative;
3441
3441
  let searched;
3442
- if (isRawAddress || explicit === void 0 && known === void 0 && !isNative) {
3443
- const matches = await search.searchTokens({ query: address, chains: [chain2] }).catch(() => []);
3442
+ if (needsDecimals || carriesRawEvmAddress(token)) {
3443
+ let matches;
3444
+ try {
3445
+ matches = await search.searchTokens({ query: address, chains: [chain2] });
3446
+ } catch (err) {
3447
+ if (needsDecimals) {
3448
+ throw new CommandError(
3449
+ "NETWORK_ERROR",
3450
+ `Could not look up the decimals of ${token} on ${chain2}: the token search failed (${errorMessage(err)}). Retry.`
3451
+ );
3452
+ }
3453
+ matches = [];
3454
+ }
3444
3455
  const match = matches.find((m2) => m2.address.toLowerCase() === address.toLowerCase());
3445
3456
  if (match) {
3446
3457
  if (match.address) address = match.address;
3447
3458
  if (typeof match.decimals === "number") searched = match.decimals;
3448
3459
  }
3449
3460
  }
3450
- const decimals = explicit ?? searched ?? known;
3451
- if (decimals === void 0) {
3452
- throw new CommandError(
3453
- "VALIDATION_ERROR",
3454
- `Could not determine decimals for ${token} on ${chain2}; provide decimals explicitly.`
3455
- );
3461
+ const resolved = known !== void 0 ? { address, decimals: known, source: "registry" } : searched !== void 0 ? { address, decimals: searched, source: "search" } : void 0;
3462
+ if (resolved) {
3463
+ if (explicit !== void 0 && explicit !== resolved.decimals) {
3464
+ throw new CommandError(
3465
+ "VALIDATION_ERROR",
3466
+ `${token} on ${chain2} has ${resolved.decimals} decimals (from the ${resolved.source === "registry" ? "token registry" : "token search"}), but ${explicit} was passed. Omit decimals for tokens twak knows, and check that this is the token you meant.`
3467
+ );
3468
+ }
3469
+ return resolved;
3456
3470
  }
3457
- return { address, decimals };
3471
+ if (explicit !== void 0) return { address, decimals: explicit, source: "caller" };
3472
+ throw new CommandError(
3473
+ "VALIDATION_ERROR",
3474
+ `Could not determine decimals for ${token} on ${chain2}: twak does not know this token. Provide decimals explicitly if you know them.`
3475
+ );
3458
3476
  }
3459
3477
  async function resolveAddress(search, chain2, params) {
3460
3478
  const { token, address } = params;
@@ -6453,7 +6471,6 @@ __export(dist_exports, {
6453
6471
  getAmberDomainsByCoinId: () => getAmberDomainsByCoinId,
6454
6472
  getCoinType: () => getCoinType,
6455
6473
  getWalletCore: () => getWalletCore,
6456
- humanAmountToWei: () => humanAmountToWei,
6457
6474
  initTrustSDK: () => initTrustSDK,
6458
6475
  isInitialized: () => isInitialized,
6459
6476
  patchNaasSession: () => patchNaasSession,
@@ -7060,13 +7077,6 @@ function chainKeyToDefinedChain(chainKey) {
7060
7077
  const chain2 = isTestnet(chainKey) ? new import_trust_sdk3.CustomChain(ct5, chainConfig.chainId) : new import_trust_sdk3.DefinedChain(ct5);
7061
7078
  return { chain: chain2, chainConfig };
7062
7079
  }
7063
- function humanAmountToWei(amount, decimals = 18) {
7064
- const parts = amount.split(".");
7065
- const whole = BigInt(parts[0] || "0");
7066
- if (parts.length === 1) return whole * 10n ** BigInt(decimals);
7067
- const frac = parts[1].padEnd(decimals, "0").slice(0, decimals);
7068
- return whole * 10n ** BigInt(decimals) + BigInt(frac);
7069
- }
7070
7080
  function createPendingTransaction(client, chainKey, hash) {
7071
7081
  return {
7072
7082
  hash,
@@ -7109,16 +7119,6 @@ function requireCoinType(coinId) {
7109
7119
  }
7110
7120
  return ct5;
7111
7121
  }
7112
- function getCoinDecimals(coinId) {
7113
- try {
7114
- const wc3 = getWalletCore();
7115
- const ct5 = requireCoinType(coinId);
7116
- return wc3.CoinTypeConfiguration.getDecimals(ct5);
7117
- } catch {
7118
- const chainKey = mainnetChainKeyForCoinId(coinId);
7119
- return chainKey ? chainOf(chainKey).decimals : 18;
7120
- }
7121
- }
7122
7122
  function tokenTypeForCoinId(coinId) {
7123
7123
  switch (coinId) {
7124
7124
  // Tron
@@ -7627,25 +7627,15 @@ var init_dist3 = __esm({
7627
7627
  const ct5 = requireCoinType(coinId);
7628
7628
  const account = this.getAccountFromSigner(chainKey);
7629
7629
  const asset = contractAddress ? new import_trust_sdk2.Asset.Token(account, contractAddress, tokenTypeForCoinId(coinId)) : new import_trust_sdk2.Asset.Coin(account);
7630
- let decimals;
7631
- if (params.decimals !== void 0) {
7632
- decimals = params.decimals;
7633
- } else if (contractAddress) {
7634
- const known = lookupKnownTokenDecimals(chainKey, contractAddress);
7635
- if (known === void 0) {
7636
- throw new TwakValidationError(
7637
- `Cannot determine decimals for ${params.assetId}; pass decimals explicitly.`
7638
- );
7639
- }
7640
- decimals = known;
7641
- } else {
7642
- decimals = getCoinDecimals(coinId);
7630
+ if (params.atomicAmount < 0n) {
7631
+ throw new TwakValidationError(
7632
+ `atomicAmount must not be negative, got ${params.atomicAmount}.`
7633
+ );
7643
7634
  }
7644
- const amountWei = humanAmountToWei(params.amount, decimals);
7645
7635
  const transaction = new import_trust_sdk2.TransactionTransfer(
7646
7636
  params.to,
7647
7637
  asset,
7648
- amountWei.toString(),
7638
+ params.atomicAmount.toString(),
7649
7639
  false,
7650
7640
  params.memo || null
7651
7641
  );
@@ -7722,7 +7712,7 @@ var init_dist3 = __esm({
7722
7712
  const transaction = new import_trust_sdk2.TransactionTransfer(
7723
7713
  params.to,
7724
7714
  asset,
7725
- params.amount,
7715
+ params.atomicAmount.toString(),
7726
7716
  false,
7727
7717
  params.memo || null
7728
7718
  );
@@ -8352,7 +8342,7 @@ var init_dist3 = __esm({
8352
8342
  const result = await this.blockchainClient.transfer({
8353
8343
  assetId,
8354
8344
  to: params.to,
8355
- amount: params.value || "0",
8345
+ atomicAmount: 0n,
8356
8346
  hdWallet,
8357
8347
  chainKey
8358
8348
  });
@@ -41911,10 +41901,13 @@ var WalletConnectProvider = class extends WalletProvider {
41911
41901
  `transfer on ${chainKey} is not supported in WalletConnect mode (EVM-only). Switch to local wallet mode for ${chainKey} transfers.`
41912
41902
  );
41913
41903
  }
41914
- if (request.decimals === void 0) {
41915
- throw new TransferError("VALIDATION_ERROR", `Decimals are required for ${request.assetId}`);
41904
+ const { atomicAmount } = request;
41905
+ if (atomicAmount < 0n) {
41906
+ throw new TransferError(
41907
+ "VALIDATION_ERROR",
41908
+ `atomicAmount must not be negative, got ${atomicAmount}.`
41909
+ );
41916
41910
  }
41917
- const atomicAmount = BigInt(parseAmount(request.amount, request.decimals));
41918
41911
  const { contractAddress } = parseTokenId(request.assetId);
41919
41912
  if (contractAddress) {
41920
41913
  assertEvmAddress(contractAddress, "tokenAddress");
@@ -43309,9 +43302,9 @@ var TransferExecutor = class {
43309
43302
  `Unknown token: ${assetId} on ${chainKey}. Use a contract address (0x...) or a supported symbol.`
43310
43303
  );
43311
43304
  }
43312
- let resolvedTo;
43305
+ let resolvedTo = params.to;
43313
43306
  try {
43314
- resolvedTo = await resolveAddressOrName(params.to);
43307
+ if (params.resolveNames !== false) resolvedTo = await resolveAddressOrName(params.to);
43315
43308
  } catch (err) {
43316
43309
  const msg = errorMessage(err);
43317
43310
  if (msg.includes("Could not resolve name")) {
@@ -43336,7 +43329,7 @@ var TransferExecutor = class {
43336
43329
  isNative: !contractAddress,
43337
43330
  explicitDecimals: params.decimals ?? null
43338
43331
  });
43339
- parseAmount(params.amount, decimals);
43332
+ const atomicAmount = BigInt(parseAmount(params.amount, decimals));
43340
43333
  if (!params.safety?.skip) {
43341
43334
  const maxUsd = params.safety?.maxUsd ?? DEFAULT_MAX_TRANSFER_USD;
43342
43335
  const warnThreshold = params.safety?.warnThresholdUsd ?? DEFAULT_WARN_THRESHOLD_USD;
@@ -43371,8 +43364,7 @@ var TransferExecutor = class {
43371
43364
  chainKey,
43372
43365
  assetId,
43373
43366
  to: resolvedTo,
43374
- amount: params.amount,
43375
- decimals,
43367
+ atomicAmount,
43376
43368
  memo: params.memo
43377
43369
  });
43378
43370
  } catch (err) {
@@ -44224,9 +44216,16 @@ ${qr3}`;
44224
44216
  "Comma-separated chains: keys, aliases or CAIP-2 IDs (default: all supported EVM chains)"
44225
44217
  ).option("--timeout <seconds>", "Connection timeout in seconds", "300").option("--json", "JSON output").action(async (opts) => {
44226
44218
  try {
44219
+ const timeoutSec = Number(opts.timeout);
44220
+ if (!Number.isInteger(timeoutSec) || timeoutSec <= 0) {
44221
+ throw new WalletError(
44222
+ "VALIDATION_ERROR",
44223
+ `--timeout must be a positive whole number of seconds (got "${opts.timeout}").`
44224
+ );
44225
+ }
44226
+ const chainKeys = opts.chains ? parseChainList(opts.chains) : void 0;
44227
44227
  const projectId = await resolveWcProjectId(opts.projectId);
44228
44228
  const provider = createWalletConnectProvider(projectId, new ChainClient());
44229
- const chainKeys = opts.chains ? parseChainList(opts.chains) : void 0;
44230
44229
  info("Connecting via WalletConnect...");
44231
44230
  const result = await provider.connect(chainKeys);
44232
44231
  const qr3 = getQRString(result.uri);
@@ -44236,13 +44235,6 @@ ${qr3}`;
44236
44235
  info(`Trust Wallet: ${result.deepLinks.native}`);
44237
44236
  info(`Universal: ${result.deepLinks.universal}`);
44238
44237
  info("\nWaiting for wallet approval...");
44239
- const timeoutSec = Number(opts.timeout);
44240
- if (!Number.isInteger(timeoutSec) || timeoutSec <= 0) {
44241
- throw new WalletError(
44242
- "VALIDATION_ERROR",
44243
- `--timeout must be a positive whole number of seconds (got "${opts.timeout}").`
44244
- );
44245
- }
44246
44238
  const session = await provider.waitForSession(timeoutSec * 1e3);
44247
44239
  if (opts.json) {
44248
44240
  output(
@@ -44483,8 +44475,21 @@ var SwapClient = class {
44483
44475
  }
44484
44476
  return transactions;
44485
44477
  }
44478
+ /**
44479
+ * Search Trust's asset index, which covers mainnets only. A testnet shares
44480
+ * its mainnet's coin ID, so querying it would return mainnet assets (and
44481
+ * their decimals) for testnet addresses: testnet chains are left out of the
44482
+ * query, and a search on testnets only has no results.
44483
+ */
44486
44484
  async searchTokens(params) {
44487
- const coinIds = params.networks ? params.networks : (params.chains ?? mainnetChainKeys()).map(coinIdOf).join(",");
44485
+ let coinIds;
44486
+ if (params.networks) {
44487
+ coinIds = params.networks;
44488
+ } else {
44489
+ const chains = (params.chains ?? mainnetChainKeys()).filter((c3) => !isTestnet(c3));
44490
+ if (chains.length === 0) return [];
44491
+ coinIds = chains.map(coinIdOf).join(",");
44492
+ }
44488
44493
  const queryParams = new URLSearchParams({
44489
44494
  networks: coinIds,
44490
44495
  exactQuery: params.query,
@@ -44837,7 +44842,7 @@ function registerTransferCommand(program3) {
44837
44842
  "Chain key (e.g., base, ethereum). When set, --token is a token contract address; omit --token to send the native coin."
44838
44843
  ).option(
44839
44844
  "--decimals <n>",
44840
- "Token decimals (overrides auto-resolution for tokens not in the registry)"
44845
+ "Token decimals, only for a token twak does not know (rejected if it contradicts the registry or token search)"
44841
44846
  ).option(
44842
44847
  "--confirm-to <address>",
44843
44848
  "Pin the expected resolved address \u2014 transfer is rejected if resolution returns a different address"
@@ -44943,7 +44948,7 @@ init_dist();
44943
44948
  function registerSwapCommand(program3) {
44944
44949
  attachPaymasterOption(program3.command("swap")).description("Quote or execute a token swap").argument("<amountOrFrom>", "Amount to swap, or source token when using --usd").argument("<fromOrTo>", "Source token, or destination token when using --usd").argument("[to]", "Destination token (omit when using --usd with 2 args)").option("--chain <id>", "Chain", "ethereum").option("--to-chain <id>", "Destination chain (for cross-chain swaps)").option("--slippage <pct>", "Slippage tolerance %", "1").option(
44945
44950
  "--decimals <n>",
44946
- "Source token decimals (overrides auto-resolution for tokens not in the registry)"
44951
+ "Source token decimals, only for a token twak does not know (rejected if it contradicts the registry or token search)"
44947
44952
  ).option("--usd <amount>", "Swap a USD-equivalent amount of the source token").option("--quote-only", "Get quote without executing").option("--password <password>", "Wallet password (required to execute)").option("--json", "JSON output").action(
44948
44953
  async (amountOrFrom, fromOrTo, toOrUndefined, opts) => {
44949
44954
  try {
@@ -47168,7 +47173,6 @@ var import_zod11 = require("zod");
47168
47173
  init_dist();
47169
47174
  var import_zod12 = require("zod");
47170
47175
  init_dist();
47171
- init_dist4();
47172
47176
  var import_zod13 = require("zod");
47173
47177
  init_dist();
47174
47178
  var import_zod14 = require("zod");
@@ -48002,9 +48006,34 @@ var ActionProvider = class {
48002
48006
  var ActionRegistry = class {
48003
48007
  providers = [];
48004
48008
  actions = /* @__PURE__ */ new Map();
48009
+ withheld = /* @__PURE__ */ new Set();
48010
+ readOnly;
48011
+ constructor(options = {}) {
48012
+ this.readOnly = options.readOnly ?? false;
48013
+ }
48014
+ /**
48015
+ * Throws when an action name is already registered (served or withheld) or
48016
+ * repeated within the provider: two actions under one name is a wiring bug,
48017
+ * and letting one silently replace the other would change what a tool name
48018
+ * does without notice. Nothing is registered when it throws.
48019
+ */
48005
48020
  register(provider) {
48021
+ const actions = provider.getActions();
48022
+ const seen = /* @__PURE__ */ new Set();
48023
+ for (const { name: name2 } of actions) {
48024
+ if (seen.has(name2) || this.actions.has(name2) || this.withheld.has(name2)) {
48025
+ throw new Error(
48026
+ `Action "${name2}" from provider "${provider.name}" is already registered; action names must be unique.`
48027
+ );
48028
+ }
48029
+ seen.add(name2);
48030
+ }
48006
48031
  this.providers.push(provider);
48007
- for (const action of provider.getActions()) {
48032
+ for (const action of actions) {
48033
+ if (this.readOnly && !action.annotations.readOnly) {
48034
+ this.withheld.add(action.name);
48035
+ continue;
48036
+ }
48008
48037
  this.actions.set(action.name, action);
48009
48038
  }
48010
48039
  }
@@ -48015,12 +48044,32 @@ var ActionRegistry = class {
48015
48044
  getAction(name2) {
48016
48045
  return this.actions.get(name2);
48017
48046
  }
48047
+ /**
48048
+ * The error for a call to an action that read-only mode withheld, so the
48049
+ * caller learns why instead of seeing an unknown action. Undefined for any
48050
+ * other name.
48051
+ */
48052
+ withheldError(name2) {
48053
+ if (!this.withheld.has(name2)) return void 0;
48054
+ return {
48055
+ code: "READ_ONLY",
48056
+ message: `${name2} is not available: this server is read-only (twak serve --read-only) and serves only tools that read. Restart it without --read-only to use ${name2}.`
48057
+ };
48058
+ }
48018
48059
  getProviders() {
48019
48060
  return this.providers;
48020
48061
  }
48021
48062
  };
48022
48063
  var CHAIN_DESC = 'Chain (e.g., "ethereum", "bsc", "solana"). Accepts chain key, CAIP-2 ("eip155:1"), EVM chainId ("56", "0x38"), aliases ("matic", "op"), and asset IDs ("c60").';
48023
48064
  var TOKEN_ADDR_DESC = "Token contract address (ERC-20 / SPL). Omit for the chain native coin (ETH, BNB, SOL, etc.).";
48065
+ var READS_REMOTE = { readOnly: true, openWorld: true };
48066
+ var READS_LOCAL = { readOnly: true, openWorld: false };
48067
+ var SIGNS = {
48068
+ readOnly: false,
48069
+ destructive: true,
48070
+ idempotent: false,
48071
+ openWorld: true
48072
+ };
48024
48073
  var BalanceActionProvider = class extends ActionProvider {
48025
48074
  name = "balance";
48026
48075
  description = "Check token balances and holdings";
@@ -48030,6 +48079,7 @@ var BalanceActionProvider = class extends ActionProvider {
48030
48079
  getBalance() {
48031
48080
  return {
48032
48081
  name: "get_balance",
48082
+ annotations: READS_REMOTE,
48033
48083
  description: 'Read the on-chain balance of a single asset for a wallet address. Omit `tokenAddress` for the chain native coin (ETH, BNB, SOL, BTC, ...). Provide `tokenAddress` to read an ERC-20 / SPL token (e.g. tokenAddress="0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" with chain="base" for USDC on Base).',
48034
48084
  schema: import_zod3.z.object({
48035
48085
  address: import_zod3.z.string().describe("Wallet address on the specified chain."),
@@ -48045,6 +48095,7 @@ var BalanceActionProvider = class extends ActionProvider {
48045
48095
  getTokenHoldings() {
48046
48096
  return {
48047
48097
  name: "get_token_holdings",
48098
+ annotations: READS_REMOTE,
48048
48099
  description: "Get ERC-20 / SPL token holdings for a wallet address on a chain. Returns the list of tokens the address holds. Omit `tokenAddress` to list all token holdings on the chain.",
48049
48100
  schema: import_zod3.z.object({
48050
48101
  address: import_zod3.z.string().describe("Wallet address on the specified chain."),
@@ -48092,6 +48143,7 @@ var BalanceActionProvider = class extends ActionProvider {
48092
48143
  searchAssets() {
48093
48144
  return {
48094
48145
  name: "search_assets",
48146
+ annotations: READS_REMOTE,
48095
48147
  description: "Search for tokens by name, symbol, or address",
48096
48148
  schema: import_zod3.z.object({
48097
48149
  query: import_zod3.z.string().describe("Search query"),
@@ -48133,6 +48185,7 @@ var TransferActionProvider = class extends ActionProvider {
48133
48185
  transfer() {
48134
48186
  return {
48135
48187
  name: "transfer",
48188
+ annotations: SIGNS,
48136
48189
  description: 'Transfer native or ERC-20 tokens to a destination address. Resolves ENS for the "to" field, applies a USD safety cap, and works with both the local agent wallet (any supported chain \u2014 EVM, Solana, Bitcoin, \u2026) and an external WalletConnect-paired wallet (EVM chains only).',
48137
48190
  schema: import_zod4.z.object({
48138
48191
  chain: import_zod4.z.string().describe(CHAIN_DESC),
@@ -48140,7 +48193,7 @@ var TransferActionProvider = class extends ActionProvider {
48140
48193
  amount: import_zod4.z.string().describe('Amount in human-readable format (e.g., "1.5", "0.01")'),
48141
48194
  tokenAddress: import_zod4.z.string().trim().min(1).optional().describe(TOKEN_ADDR_DESC),
48142
48195
  decimals: import_zod4.z.coerce.number().int().min(0).max(36).optional().describe(
48143
- "Token decimals (non-negative integer). Omit for native coins and for tokens twak recognizes \u2014 decimals are derived from the registry or token search. REQUIRED when tokenAddress is a contract twak does not recognize; look up the value on the token contract."
48196
+ "Token decimals, only for a token twak does not know. Omit it otherwise: twak resolves decimals from its registry or the Trust token search, and rejects a value that contradicts them. Needed only when the transfer fails asking for decimals."
48144
48197
  ),
48145
48198
  memo: import_zod4.z.string().optional().describe("Optional memo/note (local agent wallet only)."),
48146
48199
  confirmTo: import_zod4.z.string().optional().describe(
@@ -48201,6 +48254,7 @@ var SwapActionProvider = class extends ActionProvider {
48201
48254
  swap() {
48202
48255
  return {
48203
48256
  name: "swap",
48257
+ annotations: SIGNS,
48204
48258
  description: "Execute a token swap via Trust Wallet (supports all chains including cross-chain)",
48205
48259
  schema: import_zod5.z.object({
48206
48260
  fromChain: import_zod5.z.string().describe(`Source chain. ${CHAIN_DESC}`),
@@ -48278,6 +48332,7 @@ var SwapActionProvider = class extends ActionProvider {
48278
48332
  getSwapQuote() {
48279
48333
  return {
48280
48334
  name: "get_swap_quote",
48335
+ annotations: READS_REMOTE,
48281
48336
  description: "Get a swap quote without executing",
48282
48337
  schema: import_zod5.z.object({
48283
48338
  fromChain: import_zod5.z.string().describe(`Source chain. ${CHAIN_DESC}`),
@@ -48346,6 +48401,7 @@ var SwapActionProvider = class extends ActionProvider {
48346
48401
  getTokenPrice() {
48347
48402
  return {
48348
48403
  name: "get_token_price",
48404
+ annotations: READS_REMOTE,
48349
48405
  description: "Get current USD price for a token",
48350
48406
  schema: import_zod5.z.object({
48351
48407
  chain: import_zod5.z.string().describe(CHAIN_DESC),
@@ -48398,6 +48454,7 @@ var SecurityActionProvider = class extends ActionProvider {
48398
48454
  validateTransaction() {
48399
48455
  return {
48400
48456
  name: "validate_transaction",
48457
+ annotations: READS_REMOTE,
48401
48458
  description: "Validate an address or transaction for security. Provide `chain` (and optional `tokenAddress`) to scope the validation to a specific asset; omit both for a generic address check.",
48402
48459
  schema: import_zod6.z.object({
48403
48460
  address: import_zod6.z.string().describe("Address to validate"),
@@ -48421,6 +48478,7 @@ var SecurityActionProvider = class extends ActionProvider {
48421
48478
  getAssetInfo() {
48422
48479
  return {
48423
48480
  name: "get_asset_info",
48481
+ annotations: READS_REMOTE,
48424
48482
  description: "Get detailed information about an asset (name, symbol, price, market cap). Omit `tokenAddress` for the chain native coin.",
48425
48483
  schema: import_zod6.z.object({
48426
48484
  chain: import_zod6.z.string().describe(CHAIN_DESC),
@@ -48449,6 +48507,7 @@ var HistoryActionProvider = class extends ActionProvider {
48449
48507
  getTransactionHistory() {
48450
48508
  return {
48451
48509
  name: "get_transaction_history",
48510
+ annotations: READS_REMOTE,
48452
48511
  description: "Get transaction history for an address",
48453
48512
  schema: import_zod7.z.object({
48454
48513
  address: import_zod7.z.string().describe("Wallet address"),
@@ -48466,6 +48525,7 @@ var HistoryActionProvider = class extends ActionProvider {
48466
48525
  getTransactionDetails() {
48467
48526
  return {
48468
48527
  name: "get_transaction_details",
48528
+ annotations: READS_REMOTE,
48469
48529
  description: "Get details of a specific transaction by hash",
48470
48530
  schema: import_zod7.z.object({
48471
48531
  hash: import_zod7.z.string().describe("Transaction hash"),
@@ -48482,6 +48542,7 @@ var HistoryActionProvider = class extends ActionProvider {
48482
48542
  getSwapHistory() {
48483
48543
  return {
48484
48544
  name: "get_swap_history",
48545
+ annotations: READS_REMOTE,
48485
48546
  description: "Get swap transaction history",
48486
48547
  schema: import_zod7.z.object({
48487
48548
  address: import_zod7.z.string().describe("Wallet address"),
@@ -48509,6 +48570,7 @@ var AlertActionProvider = class extends ActionProvider {
48509
48570
  createAlert() {
48510
48571
  return {
48511
48572
  name: "create_alert",
48573
+ annotations: { readOnly: false, destructive: false, idempotent: false, openWorld: false },
48512
48574
  description: "Create a price alert. Triggers when the asset price crosses a target. Omit `tokenAddress` to watch the chain native coin (ETH, BNB, SOL, etc.).",
48513
48575
  schema: import_zod8.z.object({
48514
48576
  chain: import_zod8.z.string().describe(CHAIN_DESC),
@@ -48535,6 +48597,7 @@ var AlertActionProvider = class extends ActionProvider {
48535
48597
  listAlerts() {
48536
48598
  return {
48537
48599
  name: "list_alerts",
48600
+ annotations: READS_LOCAL,
48538
48601
  description: "List all price alerts",
48539
48602
  schema: import_zod8.z.object({
48540
48603
  activeOnly: import_zod8.z.boolean().optional().default(false).describe("Only show active alerts")
@@ -48549,6 +48612,9 @@ var AlertActionProvider = class extends ActionProvider {
48549
48612
  checkAlerts() {
48550
48613
  return {
48551
48614
  name: "check_alerts",
48615
+ // Destructive: an alert that fires is deactivated, so later checks and
48616
+ // the watcher no longer evaluate it.
48617
+ annotations: { readOnly: false, destructive: true, idempotent: false, openWorld: true },
48552
48618
  description: "Check active alerts against current prices and trigger any that match",
48553
48619
  schema: import_zod8.z.object({}),
48554
48620
  invoke: async (ctx, _input) => {
@@ -48578,6 +48644,7 @@ var AlertActionProvider = class extends ActionProvider {
48578
48644
  deleteAlert() {
48579
48645
  return {
48580
48646
  name: "delete_alert",
48647
+ annotations: { readOnly: false, destructive: true, idempotent: true, openWorld: false },
48581
48648
  description: "Delete a price alert by ID",
48582
48649
  schema: import_zod8.z.object({
48583
48650
  id: import_zod8.z.string().describe("Alert ID to delete")
@@ -48608,6 +48675,7 @@ var AgentWalletActionProvider = class extends ActionProvider {
48608
48675
  getAddress() {
48609
48676
  return {
48610
48677
  name: "get_address",
48678
+ annotations: READS_LOCAL,
48611
48679
  description: "Get the active wallet's address for a specific chain.",
48612
48680
  schema: import_zod9.z.object({
48613
48681
  chain: import_zod9.z.string().describe(CHAIN_DESC)
@@ -48623,6 +48691,7 @@ var AgentWalletActionProvider = class extends ActionProvider {
48623
48691
  listAddresses() {
48624
48692
  return {
48625
48693
  name: "list_addresses",
48694
+ annotations: READS_LOCAL,
48626
48695
  description: "Get the active wallet's addresses for every supported chain. Returns one address per chain.",
48627
48696
  schema: import_zod9.z.object({}),
48628
48697
  invoke: async () => {
@@ -48638,6 +48707,7 @@ var AgentWalletActionProvider = class extends ActionProvider {
48638
48707
  walletBalance() {
48639
48708
  return {
48640
48709
  name: "wallet_balance",
48710
+ annotations: READS_REMOTE,
48641
48711
  description: "Read the native-coin balance (BNB on BSC, ETH on Ethereum / Base / Arbitrum, SOL on Solana, etc.) for the active wallet on a specific chain. For ERC-20 / SPL token balances, use `get_balance`.",
48642
48712
  schema: import_zod9.z.object({
48643
48713
  chain: import_zod9.z.string().describe(CHAIN_DESC)
@@ -48654,6 +48724,7 @@ var AgentWalletActionProvider = class extends ActionProvider {
48654
48724
  walletPortfolio() {
48655
48725
  return {
48656
48726
  name: "wallet_portfolio",
48727
+ annotations: READS_REMOTE,
48657
48728
  description: "Read the active wallet's full portfolio across chains: native-coin and known-token balances, each with its USD value, plus the total USD. Operates on the active wallet (local HD or WalletConnect-paired) \u2014 takes no address. Native rows are returned for every queried chain (even at zero balance); zero-balance token rows are omitted. Use `wallet_balance` for a single native balance or `get_balance` for one specific token.",
48658
48729
  schema: import_zod9.z.object({
48659
48730
  chains: import_zod9.z.string().optional().describe(
@@ -48689,6 +48760,7 @@ var TokenActionProvider = class extends ActionProvider {
48689
48760
  tokenBalance() {
48690
48761
  return {
48691
48762
  name: "token_balance",
48763
+ annotations: READS_REMOTE,
48692
48764
  description: "Get balance for a specific token or native asset on a chain.",
48693
48765
  schema: import_zod10.z.object({
48694
48766
  chain: import_zod10.z.string().describe(CHAIN_DESC),
@@ -48708,6 +48780,7 @@ var TokenActionProvider = class extends ActionProvider {
48708
48780
  approveToken() {
48709
48781
  return {
48710
48782
  name: "approve_token",
48783
+ annotations: SIGNS,
48711
48784
  description: `Approve a spender to use ERC-20 tokens on your behalf. SAFETY: when amount is "unlimited" (or the MaxUint256 decimal string), you MUST surface the risk to the user and get their explicit consent before calling this tool \u2014 unlimited approvals grant the spender access to ALL of the user's tokens of this type, including future transfers, and have been used in many drainer attacks. Prefer a finite amount equal to the immediate need. Use revoke_token_approval (or amount: "0") to revoke an existing approval.`,
48712
48785
  schema: import_zod10.z.object({
48713
48786
  chain: import_zod10.z.string().describe(CHAIN_DESC),
@@ -48739,6 +48812,7 @@ var TokenActionProvider = class extends ActionProvider {
48739
48812
  revokeTokenApproval() {
48740
48813
  return {
48741
48814
  name: "revoke_token_approval",
48815
+ annotations: SIGNS,
48742
48816
  description: "Revoke an ERC-20 token approval by setting the spender allowance to 0. Use this to remove a previously-granted allowance \u2014 especially important after unlimited approvals or any approval you no longer need. This is the inverse of approve_token.",
48743
48817
  schema: import_zod10.z.object({
48744
48818
  chain: import_zod10.z.string().describe(CHAIN_DESC),
@@ -48768,6 +48842,7 @@ var TokenActionProvider = class extends ActionProvider {
48768
48842
  checkAllowance() {
48769
48843
  return {
48770
48844
  name: "check_allowance",
48845
+ annotations: READS_REMOTE,
48771
48846
  description: "Check the ERC-20 allowance for a spender address.",
48772
48847
  schema: import_zod10.z.object({
48773
48848
  chain: import_zod10.z.string().describe(CHAIN_DESC),
@@ -48808,6 +48883,7 @@ var MarketActionProvider = class extends ActionProvider {
48808
48883
  getTrendingTokens() {
48809
48884
  return {
48810
48885
  name: "get_trending_tokens",
48886
+ annotations: READS_REMOTE,
48811
48887
  description: "Get a list of trending tokens by market activity",
48812
48888
  schema: import_zod11.z.object({
48813
48889
  limit: import_zod11.z.coerce.number().optional().default(20).describe("Number of tokens to return (default 20)")
@@ -48821,6 +48897,7 @@ var MarketActionProvider = class extends ActionProvider {
48821
48897
  checkTokenRisk() {
48822
48898
  return {
48823
48899
  name: "check_token_risk",
48900
+ annotations: READS_REMOTE,
48824
48901
  description: "Check security and risk information for a token (honeypot detection, audit status, freeze authority). Omit `tokenAddress` to check the chain native coin.",
48825
48902
  schema: import_zod11.z.object({
48826
48903
  chain: import_zod11.z.string().describe(CHAIN_DESC),
@@ -48850,7 +48927,7 @@ var MarketActionProvider = class extends ActionProvider {
48850
48927
  }
48851
48928
  };
48852
48929
  var DEFAULT_MAX_OFFRAMP_USD = 1e4;
48853
- function validateOfframpInput(input2, chainKey, nativeDecimals) {
48930
+ function validateOfframpInput(input2) {
48854
48931
  if (!isEvmAddress(input2.depositAddress)) {
48855
48932
  throw new TransferError(
48856
48933
  "VALIDATION_ERROR",
@@ -48860,42 +48937,11 @@ function validateOfframpInput(input2, chainKey, nativeDecimals) {
48860
48937
  if (input2.tokenAddress && !isEvmAddress(input2.tokenAddress)) {
48861
48938
  throw new TransferError("VALIDATION_ERROR", `Malformed tokenAddress: ${input2.tokenAddress}`);
48862
48939
  }
48863
- let decimals;
48864
- if (input2.tokenAddress) {
48865
- if (input2.decimals === void 0) {
48866
- throw new TransferError(
48867
- "VALIDATION_ERROR",
48868
- `decimals is required for ERC-20 token ${input2.tokenAddress} on ${chainKey}. Pass the token's decimals explicitly (6 for USDC/USDT, 18 for DAI, etc.) \u2014 silently defaulting to 18 could over-pay the provider by orders of magnitude.`
48869
- );
48870
- }
48871
- decimals = input2.decimals;
48872
- } else {
48873
- decimals = nativeDecimals;
48874
- }
48875
48940
  try {
48876
- assertStrictPositiveAmount(input2.cryptoAmount, decimals);
48941
+ assertStrictPositiveAmount(input2.cryptoAmount);
48877
48942
  } catch (err) {
48878
48943
  throw new TransferError("VALIDATION_ERROR", errorMessage(err));
48879
48944
  }
48880
- return decimals;
48881
- }
48882
- async function checkOfframpUsdCap(priceLookup, maxUsd, chainKey, tokenAddress, cryptoAmount) {
48883
- try {
48884
- const tokenInfo = tokenAddress ? resolveToken(tokenAddress, chainKey) : void 0;
48885
- const lookupAddress2 = tokenInfo?.address ?? tokenAddress ?? "";
48886
- const price = await priceLookup.getTokenPrice(chainKey, lookupAddress2);
48887
- if (price && price > 0) {
48888
- const usdValue = parseFloat(cryptoAmount) * price;
48889
- if (usdValue > maxUsd) {
48890
- return {
48891
- code: "USD_LIMIT_EXCEEDED",
48892
- message: `Off-ramp payout value ~$${usdValue.toFixed(2)} USD exceeds safety limit of $${maxUsd}. Use the CLI with --max-usd ${Math.ceil(usdValue)} or --skip-safety-check to proceed.`
48893
- };
48894
- }
48895
- }
48896
- } catch {
48897
- }
48898
- return void 0;
48899
48945
  }
48900
48946
  function buildFollowUp(chain2, tokenAddress, address, direction, paymentMethod) {
48901
48947
  if (!chain2) return void 0;
@@ -48953,19 +48999,16 @@ var OnrampActionProvider = class extends ActionProvider {
48953
48999
  // transfer/swap/erc20 actions already follow. Explicit `walletAddress`
48954
49000
  // overrides the derivation for agents operating on behalf of other users.
48955
49001
  //
48956
- // `priceLookup` + `maxTransferUsd` enable the same USD safety cap used by
48957
- // TransferActionProvider for the off-ramp payout path (confirm_offramp_transfer).
48958
- // Off-ramp sends crypto to a third-party deposit address, so the cap is
48959
- // strictly more important here than for peer-to-peer transfers.
48960
- // `blockchainClient` is the 4th (optional) arg, not the 1st — see
48961
- // TransferActionProvider for the rationale (avoid churning ~70 test sites).
48962
- // Normalize across action providers in a follow-up.
48963
- constructor(walletContext, priceLookup, maxTransferUsd = DEFAULT_MAX_OFFRAMP_USD, blockchainClient) {
49002
+ // The off-ramp payout (confirm_offramp_transfer) goes through the same
49003
+ // TransferExecutor as `transfer`: `swapClient` resolves the token's decimals
49004
+ // and price, and `maxTransferUsd` caps the payout. Off-ramp sends crypto to
49005
+ // a third-party deposit address, so the cap matters even more here than for
49006
+ // peer-to-peer transfers.
49007
+ constructor(walletContext, swapClient, maxTransferUsd = DEFAULT_MAX_OFFRAMP_USD) {
48964
49008
  super();
48965
49009
  this.walletContext = walletContext;
48966
- this.priceLookup = priceLookup;
49010
+ this.swapClient = swapClient;
48967
49011
  this.maxTransferUsd = maxTransferUsd;
48968
- this.blockchainClient = blockchainClient;
48969
49012
  }
48970
49013
  name = "onramp";
48971
49014
  description = "Fiat on-ramp and off-ramp \u2014 buy crypto with fiat (USD, EUR, etc.) or sell crypto for fiat payouts. On-ramp: two-step flow (get quote \u2192 get checkout URL), provider sends crypto to the destination address. Off-ramp: three-step flow (get quote \u2192 get checkout URL \u2192 confirm_offramp_transfer). The off-ramp checkout URL runs in a plain browser which has no access to wallet keys, so the payout transaction must be broadcast natively via confirm_offramp_transfer once the user has read the provider's deposit address and exact amount. Destination/source address auto-derives from the configured wallet using the asset's chain. IMPORTANT: TWAK has no background polling or completion webhook for ramps. Tool responses include a `followUp` hint telling the agent exactly which balance tool to poll (with args and timing) to observe completion.";
@@ -48991,6 +49034,7 @@ var OnrampActionProvider = class extends ActionProvider {
48991
49034
  getOnrampQuote() {
48992
49035
  return {
48993
49036
  name: "get_onramp_quote",
49037
+ annotations: READS_REMOTE,
48994
49038
  description: "Step 1 of buying crypto with fiat: get quotes from available payment providers (MoonPay, Transak, etc.) for a given fiat amount. Returns an array of quotes each with a quoteId \u2014 pass one to get_onramp_url to generate the checkout redirect.",
48995
49039
  schema: import_zod12.z.object({
48996
49040
  fiatAmount: import_zod12.z.coerce.number().positive().describe("Amount of fiat currency to spend (e.g., 100 for $100)."),
@@ -49032,6 +49076,7 @@ var OnrampActionProvider = class extends ActionProvider {
49032
49076
  getOnrampUrl() {
49033
49077
  return {
49034
49078
  name: "get_onramp_url",
49079
+ annotations: READS_REMOTE,
49035
49080
  description: "Step 2 of buying crypto with fiat: exchange a quoteId from get_onramp_quote for a browser redirect URL. The user opens that URL to complete KYC and payment with the provider; on success, the provider sends the purchased crypto to the destination address. After this tool returns a URL, poll the balance tool named in the `followUp` field to observe the incoming funds \u2014 there is no background polling, webhook, or push notification.",
49036
49081
  schema: import_zod12.z.object({
49037
49082
  quoteId: import_zod12.z.string().describe("Quote ID returned by get_onramp_quote."),
@@ -49082,6 +49127,7 @@ var OnrampActionProvider = class extends ActionProvider {
49082
49127
  getOfframpQuote() {
49083
49128
  return {
49084
49129
  name: "get_offramp_quote",
49130
+ annotations: READS_REMOTE,
49085
49131
  description: "Step 1 of selling crypto for fiat: get quotes from available providers for converting a crypto amount into fiat. Returns an array of quotes each with a quoteId \u2014 pass one to get_offramp_url to generate the checkout redirect for completing KYC and order creation. The provider page cannot sign the payout (no key access in the browser); the agent must broadcast it natively via confirm_offramp_transfer once the deposit address is known.",
49086
49132
  schema: import_zod12.z.object({
49087
49133
  cryptoAmount: import_zod12.z.coerce.number().positive().describe("Amount of crypto to sell (e.g., 0.1 for 0.1 ETH)."),
@@ -49135,6 +49181,7 @@ var OnrampActionProvider = class extends ActionProvider {
49135
49181
  getOfframpUrl() {
49136
49182
  return {
49137
49183
  name: "get_offramp_url",
49184
+ annotations: READS_REMOTE,
49138
49185
  description: "Step 2 of selling crypto for fiat: exchange a quoteId from get_offramp_quote for a browser checkout URL. The user opens that URL to complete KYC and order creation with the provider; the provider then displays a deposit address (plus a memo/tag on chains that require one) and the exact amount to send. The agent must then call confirm_offramp_transfer with those values to sign and broadcast the payout natively \u2014 the browser has no access to wallet keys and cannot sign the transfer itself. After the payout broadcasts, poll the balance tool named in the `followUp` field to observe the outgoing tx. Fiat payout arrives separately via the provider.",
49139
49186
  schema: import_zod12.z.object({
49140
49187
  quoteId: import_zod12.z.string().describe("Quote ID returned by get_offramp_quote."),
@@ -49186,6 +49233,7 @@ var OnrampActionProvider = class extends ActionProvider {
49186
49233
  confirmOfframpTransfer() {
49187
49234
  return {
49188
49235
  name: "confirm_offramp_transfer",
49236
+ annotations: SIGNS,
49189
49237
  description: "Step 3 of selling crypto for fiat: sign and broadcast the payout transaction to the provider's deposit address. Call this AFTER the user has completed KYC in the browser and can read the deposit address + exact amount the provider displayed. Works with any wallet provider that implements sendTransaction (the local agent wallet, or a WalletConnect-paired external wallet). EVM-only \u2014 non-EVM chains (BTC, Solana, Cosmos, etc.) are not supported by this action. Returns a tx hash on success; fiat payout from the provider then arrives separately (no on-chain signal for that step).",
49190
49238
  schema: import_zod12.z.object({
49191
49239
  depositAddress: import_zod12.z.string().describe(
@@ -49198,8 +49246,8 @@ var OnrampActionProvider = class extends ActionProvider {
49198
49246
  tokenAddress: import_zod12.z.string().trim().min(1).optional().describe(
49199
49247
  "ERC-20 token contract address. Omit for the chain native coin. This action is EVM-only \u2014 non-EVM tokens are rejected."
49200
49248
  ),
49201
- decimals: import_zod12.z.coerce.number().optional().describe(
49202
- "Token decimals. REQUIRED for ERC-20 assets (pass 6 for USDC/USDT, 18 for DAI, etc.) \u2014 omitting it will be rejected rather than silently defaulting to 18, because a wrong-decimals encode can over-pay the provider by orders of magnitude. Ignored for native coins."
49249
+ decimals: import_zod12.z.coerce.number().int().min(0).max(36).optional().describe(
49250
+ "Token decimals, only for a token twak does not know. Omit it otherwise: twak resolves decimals from its registry or the Trust token search, and rejects a value that contradicts them. Ignored for native coins."
49203
49251
  ),
49204
49252
  memo: import_zod12.z.string().optional().describe(
49205
49253
  "Memo / destination tag. Not used on EVM chains, so supplying one on EVM is a no-op warning. Exists in the schema for forward-compat with non-EVM chains."
@@ -49209,7 +49257,7 @@ var OnrampActionProvider = class extends ActionProvider {
49209
49257
  )
49210
49258
  }),
49211
49259
  invoke: async (_ctx, input2) => {
49212
- const signer = this.walletContext.activeWallet();
49260
+ const wallet = this.walletContext.activeWallet();
49213
49261
  const { chainKey } = resolveAsset(input2.chain);
49214
49262
  const chainDef = chainOf(chainKey);
49215
49263
  if (chainDef.namespace !== "eip155") {
@@ -49218,48 +49266,30 @@ var OnrampActionProvider = class extends ActionProvider {
49218
49266
  `confirm_offramp_transfer is EVM-only. ${chainKey} is not supported.`
49219
49267
  );
49220
49268
  }
49221
- const decimals = validateOfframpInput(input2, chainKey, chainDef.decimals);
49222
- if (this.priceLookup) {
49223
- const cap = await checkOfframpUsdCap(
49224
- this.priceLookup,
49225
- this.maxTransferUsd,
49226
- chainKey,
49227
- input2.tokenAddress,
49228
- input2.cryptoAmount
49229
- );
49230
- if (cap) return { success: false, ...cap };
49231
- }
49232
- const atomic = parseAmount(input2.cryptoAmount, decimals);
49233
- let hash;
49234
- if (input2.tokenAddress) {
49235
- if (!this.blockchainClient) {
49236
- throw new TransferError(
49237
- "WALLET_NOT_FOUND",
49238
- "Chain client not available \u2014 off-ramp token payout requires a configured blockchain client"
49239
- );
49240
- }
49241
- const pending = await this.blockchainClient.contract(erc20).withAddress(input2.tokenAddress, chainKey).write.transfer([input2.depositAddress, BigInt(atomic)], signer);
49242
- hash = pending.hash;
49243
- } else {
49244
- hash = await signer.sendTransaction({
49245
- chainKey,
49246
- to: input2.depositAddress,
49247
- data: "0x",
49248
- value: "0x" + BigInt(atomic).toString(16)
49249
- });
49269
+ validateOfframpInput(input2);
49270
+ const result = await new TransferExecutor({ wallet, swapClient: this.swapClient }).execute({
49271
+ chain: chainKey,
49272
+ to: input2.depositAddress,
49273
+ resolveNames: false,
49274
+ amount: input2.cryptoAmount,
49275
+ tokenAddress: input2.tokenAddress,
49276
+ decimals: input2.decimals,
49277
+ safety: { maxUsd: this.maxTransferUsd }
49278
+ });
49279
+ if (!result.success) {
49280
+ return { success: false, code: result.code, message: result.message };
49250
49281
  }
49251
- const explorerUrl = getExplorerTxUrl(chainKey, hash) || void 0;
49252
49282
  return {
49253
49283
  success: true,
49254
- hash,
49255
- chain: chainKey,
49284
+ hash: result.hash,
49285
+ chain: result.chainKey,
49256
49286
  tokenAddress: input2.tokenAddress,
49257
- to: input2.depositAddress,
49287
+ to: result.to,
49258
49288
  amount: input2.cryptoAmount,
49259
49289
  memoIgnored: input2.memo ? `A memo was supplied (${input2.memo}) but EVM transactions have no memo field \u2014 it was ignored. If the provider requires a memo, they likely expect a non-EVM chain.` : void 0,
49260
49290
  quoteId: input2.quoteId,
49261
- explorer: explorerUrl,
49262
- summary: `Off-ramp payout of ${input2.cryptoAmount}${input2.tokenAddress ? ` ${input2.tokenAddress.slice(0, 10)}\u2026` : ` ${chainDef.symbol}`} broadcast to ${input2.depositAddress} on ${chainKey}. Fiat payout from the provider arrives separately.`
49291
+ explorer: result.explorerUrl,
49292
+ summary: `Off-ramp payout of ${input2.cryptoAmount}${input2.tokenAddress ? ` ${input2.tokenAddress.slice(0, 10)}\u2026` : ` ${chainDef.symbol}`} broadcast to ${result.to} on ${chainKey}. Fiat payout from the provider arrives separately.`
49263
49293
  };
49264
49294
  }
49265
49295
  };
@@ -49287,6 +49317,8 @@ var AutomateActionProvider = class extends ActionProvider {
49287
49317
  createAutomation() {
49288
49318
  return {
49289
49319
  name: "create_automation",
49320
+ // Destructive: the automation later trades with the agent wallet, unattended.
49321
+ annotations: { readOnly: false, destructive: true, idempotent: false, openWorld: false },
49290
49322
  description: "Create a DCA (recurring buy) or limit order (buy/sell when a price condition is met). The rule is saved immediately but only EXECUTES while a watcher is polling \u2014 either this MCP server was started with `twak serve --watch`, or a separate `twak watch` process is running. With no watcher, the rule stays pending and nothing is swapped; use run_automation_now to execute one on demand (only available while no watcher is running). Execution uses the local agent wallet; the watcher stays idle in WalletConnect mode.",
49291
49323
  schema: import_zod13.z.object({
49292
49324
  type: import_zod13.z.enum(["dca", "limit"]).describe("Automation type: dca (recurring) or limit (price trigger)"),
@@ -49336,6 +49368,7 @@ var AutomateActionProvider = class extends ActionProvider {
49336
49368
  listAutomations() {
49337
49369
  return {
49338
49370
  name: "list_automations",
49371
+ annotations: READS_LOCAL,
49339
49372
  description: 'List automation rules (DCA and limit orders). Note: "active" means scheduled, not necessarily firing \u2014 rules only execute while a watcher runs (`twak serve --watch` or `twak watch`).',
49340
49373
  schema: import_zod13.z.object({
49341
49374
  activeOnly: import_zod13.z.boolean().optional().default(true).describe("Only show active automations")
@@ -49351,6 +49384,7 @@ var AutomateActionProvider = class extends ActionProvider {
49351
49384
  deleteAutomation() {
49352
49385
  return {
49353
49386
  name: "delete_automation",
49387
+ annotations: { readOnly: false, destructive: true, idempotent: true, openWorld: false },
49354
49388
  description: "Delete (deactivate) an automation rule",
49355
49389
  schema: import_zod13.z.object({
49356
49390
  id: import_zod13.z.string().describe("Automation ID to delete")
@@ -49368,6 +49402,7 @@ var AutomateActionProvider = class extends ActionProvider {
49368
49402
  runAutomationNow() {
49369
49403
  return {
49370
49404
  name: "run_automation_now",
49405
+ annotations: SIGNS,
49371
49406
  description: "Execute an automation immediately \u2014 runs the swap now regardless of its schedule. Only available while no watcher is running: when this server was started with `serve --watch` the watcher owns execution, the tool is hidden, and invocations are rejected with WATCHER_ACTIVE to prevent double execution.",
49372
49407
  hidden: this.watcherActive,
49373
49408
  schema: import_zod13.z.object({
@@ -49461,7 +49496,11 @@ var X402ActionProvider = class extends ActionProvider {
49461
49496
  x402Quote() {
49462
49497
  return {
49463
49498
  name: "x402_quote",
49464
- description: 'Preview the payment requirements at an x402 payment-gated endpoint WITHOUT signing or paying. Returns the full list of payment options (each entry has chain, asset address, tokenName, amount, transferMethod, requiresApproval, and a pre-formatted `description` string). CRITICAL \u2014 after this returns, ALWAYS surface ALL entries in `accepts[]` to the user (as a short numbered list: chain \xB7 token \xB7 amount \xB7 method \xB7 "preferred" tag if applicable) and ask which to use BEFORE calling x402_request. Do not silently pick the `preferred` entry just because it is marked preferred \u2014 that is the picker default if no override is passed, but the user deserves to see every option (other chains may have cheaper gas, the user may hold balance only on a specific chain, etc.). The only exception: if you have already done a balance check and only one entry is viable, you may proceed with that one after noting the others as "unavailable (no balance)". IMPORTANT: each entry in `accepts[]` is an INDEPENDENT payment option \u2014 a unique (chain, token, transfer method) triple. Do NOT merge entries by chain (e.g. "BSC USDC via EIP-3009 or Permit2" is wrong if BSC USDC is only offered via Permit2 and a separate BSC entry for a different token offers EIP-3009). Read each entry as its own row; use the per-entry `description` field verbatim when surfacing options to the user. Once the user picks, call x402_request with the agreed maxPaymentAtomic plus optionally preferNetwork/preferMethod/preferAsset to disambiguate. INTERPRETING `requiresApproval`: `false` (EIP-3009, e.g. USDC on Base / Ethereum) \u2014 gasless. Sign the payment and you are done. No setup, no on-chain tx for the buyer, ever. `true` (Permit2, e.g. USDC on BSC) \u2014 the industry-standard Permit2 flow used by Uniswap, 1inch, and most major DeFi apps. THE FIRST x402_request to a Permit2 endpoint for a given token+chain broadcasts a one-time `approve(Permit2, MAX_UINT256)` tx (the buyer pays the gas \u2014 usually a few cents on BSC / Base, more on Ethereum mainnet). The allowance granted is unlimited for the canonical Permit2 contract at 0x000000000022D473030F116dDEE9F6B43aC78BA3. After that, every future Permit2 payment for that token is signature-only (gasless). Revocable any time via `twak erc20 revoke`. The per-call spend is still capped at the request `maxPaymentAtomic` \u2014 unlimited allowance does NOT mean unlimited per-call spend. When `requiresApproval: true` AND the buyer has not used Permit2 for that token on that chain before, the agent SHOULD tell the user concretely what is about to happen (one-time approval on <chain> for <token>, gas paid in the chain native token, then the payment) before calling x402_request. BALANCE CHECK \u2014 AFTER this quote returns and BEFORE recommending any route or showing the menu to the user, check the wallet balance for every viable entry in `accepts`. For each entry: call `get_address(chain)` to find the wallet address on that chain, then `get_balance` for the entry\'s asset (or `wallet_balance(chain)` for native coins). Filter or annotate the menu \u2014 only recommend routes where the wallet has enough of the asset to cover `amount`. If a Permit2 entry needs a first-time approval (`requiresApproval: true`), also confirm the wallet holds some native chain gas (gas amount varies by chain). If no viable route has sufficient balance, tell the user upfront \u2014 do not present a menu that includes routes they cannot pay. This avoids the failure mode where the agent recommends a chain the user has no balance on and the user finds out only after committing. No wallet required \u2014 read-only HTTPS + 402 parse. HTTPS-only; private IPs and non-HTTPS schemes are rejected.',
49499
+ // Not read-only: the request is sent with the caller's method before
49500
+ // any 402 check, so a POST or DELETE to an endpoint that does not ask
49501
+ // for payment runs as a normal request.
49502
+ annotations: { readOnly: false, destructive: true, idempotent: false, openWorld: true },
49503
+ description: 'Preview the payment requirements at an x402 payment-gated endpoint WITHOUT signing or paying. Returns the full list of payment options (each entry has chain, asset address, tokenName, amount, transferMethod, requiresApproval, and a pre-formatted `description` string). CRITICAL \u2014 after this returns, ALWAYS surface ALL entries in `accepts[]` to the user (as a short numbered list: chain \xB7 token \xB7 amount \xB7 method \xB7 "preferred" tag if applicable) and ask which to use BEFORE calling x402_request. Do not silently pick the `preferred` entry just because it is marked preferred \u2014 that is the picker default if no override is passed, but the user deserves to see every option (other chains may have cheaper gas, the user may hold balance only on a specific chain, etc.). The only exception: if you have already done a balance check and only one entry is viable, you may proceed with that one after noting the others as "unavailable (no balance)". IMPORTANT: each entry in `accepts[]` is an INDEPENDENT payment option \u2014 a unique (chain, token, transfer method) triple. Do NOT merge entries by chain (e.g. "BSC USDC via EIP-3009 or Permit2" is wrong if BSC USDC is only offered via Permit2 and a separate BSC entry for a different token offers EIP-3009). Read each entry as its own row; use the per-entry `description` field verbatim when surfacing options to the user. Once the user picks, call x402_request with the agreed maxPaymentAtomic plus optionally preferNetwork/preferMethod/preferAsset to disambiguate. INTERPRETING `requiresApproval`: `false` (EIP-3009, e.g. USDC on Base / Ethereum) \u2014 gasless. Sign the payment and you are done. No setup, no on-chain tx for the buyer, ever. `true` (Permit2, e.g. USDC on BSC) \u2014 the industry-standard Permit2 flow used by Uniswap, 1inch, and most major DeFi apps. THE FIRST x402_request to a Permit2 endpoint for a given token+chain broadcasts a one-time `approve(Permit2, MAX_UINT256)` tx (the buyer pays the gas \u2014 usually a few cents on BSC / Base, more on Ethereum mainnet). The allowance granted is unlimited for the canonical Permit2 contract at 0x000000000022D473030F116dDEE9F6B43aC78BA3. After that, every future Permit2 payment for that token is signature-only (gasless). Revocable any time via `twak erc20 revoke`. The per-call spend is still capped at the request `maxPaymentAtomic` \u2014 unlimited allowance does NOT mean unlimited per-call spend. When `requiresApproval: true` AND the buyer has not used Permit2 for that token on that chain before, the agent SHOULD tell the user concretely what is about to happen (one-time approval on <chain> for <token>, gas paid in the chain native token, then the payment) before calling x402_request. BALANCE CHECK \u2014 AFTER this quote returns and BEFORE recommending any route or showing the menu to the user, check the wallet balance for every viable entry in `accepts`. For each entry: call `get_address(chain)` to find the wallet address on that chain, then `get_balance` for the entry\'s asset (or `wallet_balance(chain)` for native coins). Filter or annotate the menu \u2014 only recommend routes where the wallet has enough of the asset to cover `amount`. If a Permit2 entry needs a first-time approval (`requiresApproval: true`), also confirm the wallet holds some native chain gas (gas amount varies by chain). If no viable route has sufficient balance, tell the user upfront \u2014 do not present a menu that includes routes they cannot pay. This avoids the failure mode where the agent recommends a chain the user has no balance on and the user finds out only after committing. No wallet required: nothing is signed or paid. The request is sent with the given method and body, so a method other than GET can change state on an endpoint that does not answer 402; quote with GET unless the endpoint requires another method. HTTPS-only; private IPs and non-HTTPS schemes are rejected.',
49465
49504
  schema: import_zod14.z.object({
49466
49505
  url: import_zod14.z.string().describe(
49467
49506
  "Target endpoint URL. Must be HTTPS. Resolves via DNS; private/local IPs are rejected."
@@ -49477,6 +49516,7 @@ var X402ActionProvider = class extends ActionProvider {
49477
49516
  x402Request() {
49478
49517
  return {
49479
49518
  name: "x402_request",
49519
+ annotations: SIGNS,
49480
49520
  description: "Make an HTTP request to an x402 payment-gated endpoint. Handles the full flow: sends the initial request, parses any 402 challenge, signs an EIP-3009 or Permit2 payment authorization with the agent wallet, retries with the X-Payment header. PAYMENT METHODS: x402 endpoints offer one or more options in their 402 response. EIP-3009 is gasless for the buyer (no setup, no approval) \u2014 preferred when available, e.g. USDC on Base / Ethereum. Permit2 needs a one-time approve(Permit2, MAX) tx on the chain (the buyer pays the gas \u2014 usually a few cents on BSC / Base, more on Ethereum mainnet), then every future Permit2 payment for that token is signature-only. Permit2 is required when the token does not support EIP-3009 \u2014 e.g. USDC on BSC. SAFETY \u2014 autoApprove decision: When autoApprove=true (default), this action silently broadcasts the one-time Permit2 approval if your wallet has not yet approved the canonical Permit2 contract for that token. The approval is to the same Permit2 contract Uniswap, 1inch, and most major DeFi apps use \u2014 it is not exotic infrastructure. The approval alone does not move funds; each subsequent payment still needs a fresh signature. Revocable any time via `twak erc20 revoke`. For first-time use of an endpoint you SHOULD tell the user concretely what is about to happen (one-time approval on <chain> for <token>, gas paid in the chain native token, then the payment) and get their go-ahead before calling. After the approval lands, subsequent Permit2 payments for that token are free of further approvals. Set autoApprove=false to surface code: 'PERMIT2_APPROVAL_REQUIRED' instead of broadcasting \u2014 useful when the user is not around or you want to handle the approval out-of-band. maxPaymentAtomic is the user's pre-commitment to a maximum payment in the asset's smallest unit (e.g. \"10000\" = 0.01 USDC at 6 decimals). If the server demands more than this cap, the call returns code: 'PAYMENT_AMOUNT_EXCEEDED' \u2014 DO NOT retry with a higher cap without asking the user. BALANCE CHECK \u2014 Before calling this action, verify the wallet holds enough of the chosen asset on the chosen chain. Call `get_address(chain)` to find the wallet address, then `get_balance` for the asset (or `wallet_balance(chain)` for native coins). For Permit2 first-time use, also confirm the wallet holds some native chain gas (amount varies by chain). Surface insufficient balance to the user BEFORE calling this action \u2014 a failed payment mid-flow can leave the wallet in a half-state (e.g. Permit2 approval broadcast, then the transfer signature fails). The balance check should already have happened during the `x402_quote` flow that produced the menu; redo it here only if you skipped quote or the balance might have changed since. WALLET \u2014 Signs through whichever wallet mode is active for this session (local by default; switchable via `switch_wallet_mode`). If mode='local', signs in-process (no external prompt). If mode='walletconnect', signs over the WalletConnect relay \u2014 the user must approve each signature on their paired wallet. To retrieve the active wallet's address for a chain (e.g. to call `wallet_balance` / `get_balance`), call `get_address` with the chain key (e.g. \"bsc\", \"base\"), or `list_addresses` for all chains at once. HTTPS-only; non-HTTPS schemes and private IPs are rejected before any payment signing.",
49481
49521
  schema: import_zod14.z.object({
49482
49522
  url: import_zod14.z.string().describe(
@@ -49591,6 +49631,9 @@ var WalletModeActionProvider = class extends ActionProvider {
49591
49631
  switchWalletMode() {
49592
49632
  return {
49593
49633
  name: "switch_wallet_mode",
49634
+ // Local mode revokes the WalletConnect session; walletconnect mode starts
49635
+ // a pairing through the relay.
49636
+ annotations: { readOnly: false, destructive: true, idempotent: false, openWorld: true },
49594
49637
  description: 'Switch the active wallet mode for this MCP server session. IMPORTANT: the LOCAL agent wallet is auto-bound at server startup when one is configured, so most users do NOT need to call this \u2014 signing actions just work. Call this ONLY when the user explicitly asks to switch. Two flows: (a) user wants to sign on their phone via Trust Wallet (or any WC-compatible wallet) \u2192 mode="walletconnect"; (b) user wants to switch back from WalletConnect to the local wallet \u2192 mode="local". \n\nDISCOVERABILITY: most users do not know WalletConnect is an option. When the user first asks about wallet operations or signing in a fresh conversation, briefly mention that they can optionally switch to signing via Trust Wallet on their phone (more secure: keys never leave their device) by saying so \u2014 then call switch_wallet_mode if they opt in. Do not push it on every signing call; just surface it once early on or when the user expresses security concerns.\n\nFor mode="walletconnect": returns a pairing payload (URI, mobile deep link, inline QR image). Surface the QR to the user, then poll get_wallet_status every few seconds until state leaves "wc-pairing": "wc-connected" means the user approved; "unbound" means the pairing ended, and lastError says why (USER_REJECTED, WC_PAIRING_EXPIRED after about 5 minutes, or WC_PAIRING_FAILED). Do not retry a signing action before "wc-connected". Do NOT call switch_wallet_mode while a signing action is in flight: switching disconnects the prior WC relay session, which can break the in-flight signature; wait for the current action to settle before changing modes.',
49595
49638
  schema: import_zod15.z.object({
49596
49639
  mode: import_zod15.z.enum(["local", "walletconnect"]).describe(
@@ -49617,6 +49660,7 @@ var WalletModeActionProvider = class extends ActionProvider {
49617
49660
  getWalletStatus() {
49618
49661
  return {
49619
49662
  name: "get_wallet_status",
49663
+ annotations: READS_LOCAL,
49620
49664
  description: 'Returns the current wallet binding for this MCP server session: state ("unbound" | "local" | "wc-pairing" | "wc-connected"), wallet type (when bound), peer name (when wc-connected), pairing data (URI / deep link / expiry, when state="wc-pairing"), `lastError` (why the wallet went away or a pairing ended), and `twakHome`: the folder this server keeps its files in (wallet, WalletConnect session, credentials), set from TWAK_HOME when the server started (default ~/.twak). After switch_wallet_mode({mode: "walletconnect"}), poll this every few seconds until state leaves "wc-pairing": "wc-connected" means the user approved and signing actions are usable; "unbound" means the pairing ended without a session, and lastError.code says why: USER_REJECTED (declined in the wallet), WC_PAIRING_EXPIRED (not approved within about 5 minutes) or WC_PAIRING_FAILED. Ask the user before starting a new pairing. While pairing, the response carries `pairing.deepLink` (tap on a phone) and the raw `pairing.uri`. The QR image is returned only with showQr=true: pass it when the user asks to see the QR again (e.g. "show me the QR", "I can\'t see it"), not when polling. Do NOT try to render the QR yourself from the URI or the response JSON.',
49621
49665
  schema: import_zod15.z.object({
49622
49666
  showQr: import_zod15.z.boolean().optional().describe(
@@ -49642,6 +49686,7 @@ var Erc8004ActionProvider = class extends ActionProvider {
49642
49686
  register() {
49643
49687
  return {
49644
49688
  name: "erc8004_register",
49689
+ annotations: SIGNS,
49645
49690
  description: "Mint a new ERC-8004 agent identity on-chain. The caller becomes the owner of an NFT token whose ID is the agentId. The supplied agentURI is stored on-chain as the token URI and should point to the agent registration JSON (https, ipfs, or a data: inline). Optionally pass metadata entries to set them atomically in the same mint transaction.",
49646
49691
  schema: import_zod16.z.object({
49647
49692
  agentURI: import_zod16.z.string().min(1).describe(
@@ -49673,6 +49718,7 @@ var Erc8004ActionProvider = class extends ActionProvider {
49673
49718
  setUri() {
49674
49719
  return {
49675
49720
  name: "erc8004_set_uri",
49721
+ annotations: SIGNS,
49676
49722
  description: "Update the agentURI of an existing ERC-8004 agent identity. Caller must own the NFT.",
49677
49723
  schema: import_zod16.z.object({
49678
49724
  agentId: agentIdSchema,
@@ -49691,6 +49737,7 @@ var Erc8004ActionProvider = class extends ActionProvider {
49691
49737
  setMetadata() {
49692
49738
  return {
49693
49739
  name: "erc8004_set_metadata",
49740
+ annotations: SIGNS,
49694
49741
  description: "Set a metadata entry on an ERC-8004 agent identity. The value is stored on-chain as bytes \u2014 `0x`-prefixed hex inputs are decoded as-is; any other string is UTF-8 encoded.",
49695
49742
  schema: import_zod16.z.object({
49696
49743
  agentId: agentIdSchema,
@@ -49714,6 +49761,7 @@ var Erc8004ActionProvider = class extends ActionProvider {
49714
49761
  getMetadata() {
49715
49762
  return {
49716
49763
  name: "erc8004_get_metadata",
49764
+ annotations: READS_REMOTE,
49717
49765
  description: "Read a metadata entry from an ERC-8004 agent identity. Returns the raw on-chain bytes as a `0x`-prefixed hex string; an unset key returns `0x` (empty bytes).",
49718
49766
  schema: import_zod16.z.object({
49719
49767
  agentId: agentIdSchema,
@@ -49736,6 +49784,7 @@ var Erc8004ActionProvider = class extends ActionProvider {
49736
49784
  show() {
49737
49785
  return {
49738
49786
  name: "erc8004_show",
49787
+ annotations: READS_REMOTE,
49739
49788
  description: "Read the on-chain state of an ERC-8004 agent identity: owner, agentWallet (or null if unset), and agentURI.",
49740
49789
  schema: import_zod16.z.object({
49741
49790
  agentId: agentIdSchema,
@@ -49802,6 +49851,7 @@ var Erc8183ActionProvider = class extends ActionProvider {
49802
49851
  createJob() {
49803
49852
  return {
49804
49853
  name: "erc8183_create_job",
49854
+ annotations: SIGNS,
49805
49855
  description: "Create a new ERC-8183 job. The caller becomes the client. The job starts in `Open` status with no escrow; set a budget then fund it to move it to `Funded`.",
49806
49856
  schema: import_zod17.z.object({
49807
49857
  provider: evmAddressSchema.describe(
@@ -49837,6 +49887,7 @@ var Erc8183ActionProvider = class extends ActionProvider {
49837
49887
  setProvider() {
49838
49888
  return {
49839
49889
  name: "erc8183_set_provider",
49890
+ annotations: SIGNS,
49840
49891
  description: "Set or change the provider (worker) address for a job (client only, while the job is still `Open`). Use the zero address to unset.",
49841
49892
  schema: import_zod17.z.object({
49842
49893
  jobId: jobIdSchema,
@@ -49858,6 +49909,7 @@ var Erc8183ActionProvider = class extends ActionProvider {
49858
49909
  setBudget() {
49859
49910
  return {
49860
49911
  name: "erc8183_set_budget",
49912
+ annotations: SIGNS,
49861
49913
  description: "Set the escrow budget for a job (atomic units of the contract payment token). Does not transfer funds \u2014 call `erc8183_fund` to deposit the budget into escrow.",
49862
49914
  schema: import_zod17.z.object({
49863
49915
  jobId: jobIdSchema,
@@ -49879,6 +49931,7 @@ var Erc8183ActionProvider = class extends ActionProvider {
49879
49931
  fund() {
49880
49932
  return {
49881
49933
  name: "erc8183_fund",
49934
+ annotations: SIGNS,
49882
49935
  description: "Fund a job: deposits the on-chain budget into escrow. The escrow pulls the payment token via transferFrom, so this first sends an ERC-20 approval for the exact budget, then calls fund \u2014 two transactions. Moves the job to `Funded`. Pass `expectedBudget` to pin the amount instead of reading it on-chain.",
49883
49936
  schema: import_zod17.z.object({
49884
49937
  jobId: jobIdSchema,
@@ -49908,6 +49961,7 @@ var Erc8183ActionProvider = class extends ActionProvider {
49908
49961
  submit() {
49909
49962
  return {
49910
49963
  name: "erc8183_submit",
49964
+ annotations: SIGNS,
49911
49965
  description: "Submit a deliverable for a funded job (provider only). The deliverable is a bytes32 commitment (e.g. a content hash). Moves the job to `Submitted`.",
49912
49966
  schema: import_zod17.z.object({
49913
49967
  jobId: jobIdSchema,
@@ -49933,6 +49987,7 @@ var Erc8183ActionProvider = class extends ActionProvider {
49933
49987
  complete() {
49934
49988
  return {
49935
49989
  name: "erc8183_complete",
49990
+ annotations: SIGNS,
49936
49991
  description: "Mark a submitted job complete (evaluator only). Releases escrow: the platform fee goes to the treasury and the remainder to the provider. Moves the job to `Completed`.",
49937
49992
  schema: import_zod17.z.object({
49938
49993
  jobId: jobIdSchema,
@@ -49960,6 +50015,7 @@ var Erc8183ActionProvider = class extends ActionProvider {
49960
50015
  reject() {
49961
50016
  return {
49962
50017
  name: "erc8183_reject",
50018
+ annotations: SIGNS,
49963
50019
  description: "Reject a job (evaluator after submission, or client while still Open). Refunds the client if the job was funded. Moves the job to `Rejected`.",
49964
50020
  schema: import_zod17.z.object({
49965
50021
  jobId: jobIdSchema,
@@ -49987,6 +50043,7 @@ var Erc8183ActionProvider = class extends ActionProvider {
49987
50043
  claimRefund() {
49988
50044
  return {
49989
50045
  name: "erc8183_claim_refund",
50046
+ annotations: SIGNS,
49990
50047
  description: "Claim a refund for an expired job (client only, after the job has passed its expiry without completion). Returns the escrowed budget to the client.",
49991
50048
  schema: import_zod17.z.object({
49992
50049
  jobId: jobIdSchema,
@@ -50008,6 +50065,7 @@ var Erc8183ActionProvider = class extends ActionProvider {
50008
50065
  status() {
50009
50066
  return {
50010
50067
  name: "erc8183_status",
50068
+ annotations: READS_REMOTE,
50011
50069
  description: "Read the on-chain state of a job: client, provider, evaluator, description, budget, status, expiry, submission, deliverable, and the contract payment token.",
50012
50070
  schema: import_zod17.z.object({
50013
50071
  jobId: jobIdSchema,
@@ -50038,6 +50096,7 @@ var Erc8183ActionProvider = class extends ActionProvider {
50038
50096
  policyInfo() {
50039
50097
  return {
50040
50098
  name: "erc8183_policy_info",
50099
+ annotations: READS_REMOTE,
50041
50100
  description: "Read the dispute-policy state of a job: the registered policy address, dispute window, submission time, dispute/vote tallies, and when the job becomes settleable (submittedAt + disputeWindow). Use before `erc8183_settle` to check timing, and before `erc8183_dispute`/`erc8183_vote_reject` to avoid opaque reverts.",
50042
50101
  schema: import_zod17.z.object({
50043
50102
  jobId: jobIdSchema,
@@ -50067,6 +50126,7 @@ var Erc8183ActionProvider = class extends ActionProvider {
50067
50126
  registerJob() {
50068
50127
  return {
50069
50128
  name: "erc8183_register_job",
50129
+ annotations: SIGNS,
50070
50130
  description: "Register a job with a dispute policy via the EvaluatorRouter. Defaults to the deployed OptimisticPolicy when no policy address is given.",
50071
50131
  schema: import_zod17.z.object({
50072
50132
  jobId: jobIdSchema,
@@ -50083,6 +50143,7 @@ var Erc8183ActionProvider = class extends ActionProvider {
50083
50143
  settle() {
50084
50144
  return {
50085
50145
  name: "erc8183_settle",
50146
+ annotations: SIGNS,
50086
50147
  description: "Settle a job through its registered policy via the EvaluatorRouter, finalising the outcome (release or refund) based on the policy verdict.",
50087
50148
  schema: import_zod17.z.object({
50088
50149
  jobId: jobIdSchema,
@@ -50099,6 +50160,7 @@ var Erc8183ActionProvider = class extends ActionProvider {
50099
50160
  markExpired() {
50100
50161
  return {
50101
50162
  name: "erc8183_mark_expired",
50163
+ annotations: SIGNS,
50102
50164
  description: "Finalise a registered job that has passed its expiry without completion, via the EvaluatorRouter (permissionless). Moves the job to `Expired`, after which the client can claim a refund.",
50103
50165
  schema: import_zod17.z.object({
50104
50166
  jobId: jobIdSchema,
@@ -50115,6 +50177,7 @@ var Erc8183ActionProvider = class extends ActionProvider {
50115
50177
  dispute() {
50116
50178
  return {
50117
50179
  name: "erc8183_dispute",
50180
+ annotations: SIGNS,
50118
50181
  description: "Raise a dispute on a job via the OptimisticPolicy (client only, within the dispute window). Opens the job to voter rejection.",
50119
50182
  schema: import_zod17.z.object({
50120
50183
  jobId: jobIdSchema,
@@ -50130,6 +50193,7 @@ var Erc8183ActionProvider = class extends ActionProvider {
50130
50193
  voteReject() {
50131
50194
  return {
50132
50195
  name: "erc8183_vote_reject",
50196
+ annotations: SIGNS,
50133
50197
  description: "Cast a reject vote on a disputed job via the OptimisticPolicy (whitelisted voters only). When the reject quorum is reached the job is rejected.",
50134
50198
  schema: import_zod17.z.object({
50135
50199
  jobId: jobIdSchema,
@@ -50172,6 +50236,8 @@ var CompetitionActionProvider = class extends ActionProvider {
50172
50236
  register() {
50173
50237
  return {
50174
50238
  name: "competition_register",
50239
+ // Idempotent: once registered, it returns without sending a transaction.
50240
+ annotations: { readOnly: false, destructive: true, idempotent: true, openWorld: true },
50175
50241
  description: "Register the agent wallet for BNB HACK: AI TRADING AGENT EDITION on BSC. Registers the agent's own signing wallet (msg.sender). Idempotent \u2014 if already registered, returns success without sending a transaction. Fails if the registration window is not open (before it opens or after the deadline).",
50176
50242
  schema: import_zod18.z.object({}),
50177
50243
  invoke: async () => {
@@ -50228,6 +50294,7 @@ var CompetitionActionProvider = class extends ActionProvider {
50228
50294
  status() {
50229
50295
  return {
50230
50296
  name: "competition_status",
50297
+ annotations: READS_REMOTE,
50231
50298
  description: "Check whether the agent wallet is registered for BNB HACK: AI TRADING AGENT EDITION, when registration opens and closes, and whether the window is currently open.",
50232
50299
  schema: import_zod18.z.object({}),
50233
50300
  invoke: async () => {
@@ -54910,6 +54977,27 @@ When the user first engages with signing operations in a fresh conversation, bri
54910
54977
  To switch mid-session, call \`switch_wallet_mode\`. The new mode applies to subsequent calls; in-flight signing keeps using its captured wallet. A WalletConnect session is stored on disk and resumed when the server restarts, until it expires (about 7 days) or is revoked; choosing local mode revokes it.
54911
54978
 
54912
54979
  If a tool returns CREDENTIALS_MISSING, the Trust Wallet API credentials are not set up yet: ask the user to run \`twak init\` (or \`twak setup\`) with their access ID and HMAC secret. The tools work as soon as the credentials are saved; no server restart is needed.`;
54980
+ var READ_ONLY_SERVER_INSTRUCTIONS = `This twak server is read-only (\`twak serve --read-only\`): it serves only tools that read, such as prices, quotes, balances, history and wallet addresses. It cannot sign, send, swap, approve, or change alerts, automations or the wallet mode; calling such a tool returns READ_ONLY. To do any of that, the user restarts \`twak serve\` without --read-only.
54981
+
54982
+ The wallet was chosen when the server started: the WalletConnect session resumed from a previous pairing if there was one, otherwise the local agent wallet. Wallet reads (get_address, list_addresses, wallet_balance, ...) use its addresses, and get_wallet_status shows which one it is. To read another wallet, the user pairs it with \`twak wallet connect\` (or switches back to the local wallet) and restarts the server. If a tool returns WALLET_NOT_BOUND or the WalletConnect session ended, tell the user that; switch_wallet_mode is not available on this server.
54983
+
54984
+ If a tool returns CREDENTIALS_MISSING, the Trust Wallet API credentials are not set up yet: ask the user to run \`twak init\` (or \`twak setup\`) with their access ID and HMAC secret. The tools work as soon as the credentials are saved; no server restart is needed.`;
54985
+ function toToolAnnotations(annotations) {
54986
+ if (annotations.readOnly) {
54987
+ return {
54988
+ readOnlyHint: true,
54989
+ destructiveHint: false,
54990
+ idempotentHint: true,
54991
+ openWorldHint: annotations.openWorld
54992
+ };
54993
+ }
54994
+ return {
54995
+ readOnlyHint: false,
54996
+ destructiveHint: annotations.destructive,
54997
+ idempotentHint: annotations.idempotent,
54998
+ openWorldHint: annotations.openWorld
54999
+ };
55000
+ }
54913
55001
  var TwakMcpServer = class {
54914
55002
  server;
54915
55003
  registry;
@@ -54921,7 +55009,10 @@ var TwakMcpServer = class {
54921
55009
  this.beforeInvoke = options.beforeInvoke;
54922
55010
  this.server = new Server(
54923
55011
  { name: "twak", version: "0.1.0" },
54924
- { capabilities: { tools: {} }, instructions: SERVER_INSTRUCTIONS }
55012
+ {
55013
+ capabilities: { tools: {} },
55014
+ instructions: registry.readOnly ? READ_ONLY_SERVER_INSTRUCTIONS : SERVER_INSTRUCTIONS
55015
+ }
54925
55016
  );
54926
55017
  this.setupHandlers();
54927
55018
  }
@@ -54932,7 +55023,8 @@ var TwakMcpServer = class {
54932
55023
  tools: actions.map((action) => ({
54933
55024
  name: action.name,
54934
55025
  description: action.description,
54935
- inputSchema: zodToJsonSchema(action.schema)
55026
+ inputSchema: zodToJsonSchema(action.schema),
55027
+ annotations: toToolAnnotations(action.annotations)
54936
55028
  }))
54937
55029
  };
54938
55030
  });
@@ -54940,16 +55032,12 @@ var TwakMcpServer = class {
54940
55032
  const { name: name2, arguments: args } = request.params;
54941
55033
  const action = this.registry.getAction(name2);
54942
55034
  if (!action) {
55035
+ const payload = this.registry.withheldError(name2) ?? {
55036
+ code: "VALIDATION_ERROR",
55037
+ message: `Unknown tool: ${name2}`
55038
+ };
54943
55039
  return {
54944
- content: [
54945
- {
54946
- type: "text",
54947
- text: JSON.stringify({
54948
- code: "VALIDATION_ERROR",
54949
- message: `Unknown tool: ${name2}`
54950
- })
54951
- }
54952
- ],
55040
+ content: [{ type: "text", text: JSON.stringify(payload) }],
54953
55041
  isError: true
54954
55042
  };
54955
55043
  }
@@ -55111,7 +55199,11 @@ var TwakRestServer = class {
55111
55199
  res.writeHead(200);
55112
55200
  res.end(
55113
55201
  JSON.stringify({
55114
- actions: actions.map((a3) => ({ name: a3.name, description: a3.description }))
55202
+ actions: actions.map((a3) => ({
55203
+ name: a3.name,
55204
+ description: a3.description,
55205
+ annotations: a3.annotations
55206
+ }))
55115
55207
  })
55116
55208
  );
55117
55209
  return;
@@ -55125,9 +55217,12 @@ var TwakRestServer = class {
55125
55217
  const actionName = match[1];
55126
55218
  const action = this.registry.getAction(actionName);
55127
55219
  if (!action) {
55128
- res.writeHead(404);
55220
+ const withheld = this.registry.withheldError(actionName);
55221
+ res.writeHead(withheld ? 403 : 404);
55129
55222
  res.end(
55130
- JSON.stringify({ code: "VALIDATION_ERROR", message: `Unknown action: ${actionName}` })
55223
+ JSON.stringify(
55224
+ withheld ?? { code: "VALIDATION_ERROR", message: `Unknown action: ${actionName}` }
55225
+ )
55131
55226
  );
55132
55227
  return;
55133
55228
  }
@@ -55262,8 +55357,16 @@ function registerServeCommand(program3) {
55262
55357
  "--watch-interval <duration>",
55263
55358
  "Automation poll interval when --watch is set \u2014 e.g. 30s, 5m, 1h (min 5s)",
55264
55359
  "60"
55360
+ ).option(
55361
+ "--read-only",
55362
+ "Serve only tools that read (prices, quotes, balances, history, addresses); tools that sign or change state are not listed and cannot be called"
55265
55363
  ).action(async (opts) => {
55266
55364
  if (!opts.rest) routeConsoleToStderr();
55365
+ if (opts.readOnly && opts.watch) {
55366
+ error("--read-only cannot be combined with --watch: the watcher signs automations");
55367
+ process.exit(1);
55368
+ return;
55369
+ }
55267
55370
  let autoLockMinutes;
55268
55371
  if (opts.autoLock) {
55269
55372
  autoLockMinutes = parseInt(opts.autoLock, 10);
@@ -55335,7 +55438,7 @@ function registerServeCommand(program3) {
55335
55438
  warn(`${errorMessage(err)} Tool calls report this error until it is resolved.`);
55336
55439
  }
55337
55440
  }
55338
- const registry = new ActionRegistry();
55441
+ const registry = new ActionRegistry({ readOnly: Boolean(opts.readOnly) });
55339
55442
  registry.register(new BalanceActionProvider());
55340
55443
  registry.register(
55341
55444
  new WalletModeActionProvider({ walletContext, agentWallet: () => walletLoader.load() })
@@ -55348,9 +55451,7 @@ function registerServeCommand(program3) {
55348
55451
  registry.register(new AgentWalletActionProvider(walletContext));
55349
55452
  registry.register(new TokenActionProvider(chainClient, walletContext));
55350
55453
  registry.register(new MarketActionProvider());
55351
- registry.register(
55352
- new OnrampActionProvider(walletContext, swapClient, void 0, chainClient)
55353
- );
55454
+ registry.register(new OnrampActionProvider(walletContext, swapClient));
55354
55455
  registry.register(
55355
55456
  new AutomateActionProvider(
55356
55457
  automationStore,
@@ -55430,7 +55531,7 @@ function registerServeCommand(program3) {
55430
55531
  const mcpServer = new TwakMcpServer(registry, context, {
55431
55532
  beforeInvoke: () => bootstrap.ensureReady()
55432
55533
  });
55433
- console.error("TW Agent MCP server starting...");
55534
+ console.error(`TW Agent MCP server starting${opts.readOnly ? " (read-only)" : ""}...`);
55434
55535
  await mcpServer.start();
55435
55536
  }
55436
55537
  });
@@ -55734,6 +55835,9 @@ ${url}`);
55734
55835
  ).requiredOption("--asset <id>", "Crypto asset ID being sold (e.g., c60 for ETH, c0 for BTC)").requiredOption("--to <address>", "Deposit address shown by the provider after KYC").requiredOption(
55735
55836
  "--amount <n>",
55736
55837
  "Exact amount to send, in human-readable format (must match the amount the provider displays)"
55838
+ ).option(
55839
+ "--decimals <n>",
55840
+ "Token decimals, only for a token twak does not know (rejected if it contradicts the registry or token search)"
55737
55841
  ).option(
55738
55842
  "--memo <tag>",
55739
55843
  "Memo / destination tag required by some chains (Cosmos, XRP, Stellar, BNB Beacon). Include this when the provider displays one \u2014 funds can be unrecoverable without it."
@@ -55764,72 +55868,47 @@ ${url}`);
55764
55868
  `Unsupported coin type in asset ID: ${opts.asset}`
55765
55869
  );
55766
55870
  }
55767
- const parsedAmount = parseFloat(opts.amount);
55768
- if (!Number.isFinite(parsedAmount) || parsedAmount <= 0) {
55769
- throw new TransferError("VALIDATION_ERROR", "--amount must be a positive number");
55770
- }
55771
- const httpClient = await getClient();
55772
- let decimals;
55773
- try {
55774
- const assetInfo = await new SecurityService(httpClient).getAssetInfo(opts.asset);
55775
- decimals = assetInfo.decimals;
55776
- } catch {
55777
- }
55778
55871
  try {
55779
- assertStrictPositiveAmount(opts.amount, decimals);
55872
+ assertStrictPositiveAmount(opts.amount);
55780
55873
  } catch (err) {
55781
55874
  throw new TransferError("VALIDATION_ERROR", errorMessage(err));
55782
55875
  }
55783
- if (!opts.skipSafetyCheck) {
55876
+ let safety;
55877
+ if (opts.skipSafetyCheck) {
55878
+ safety = { skip: true };
55879
+ } else {
55784
55880
  const maxUsd = parseFloat(opts.maxUsd ?? String(DEFAULT_MAX_OFFRAMP_USD2));
55785
55881
  if (!Number.isFinite(maxUsd) || maxUsd <= 0) {
55786
55882
  throw new TransferError("VALIDATION_ERROR", "--max-usd must be a positive number");
55787
55883
  }
55788
- try {
55789
- const tokenInfo = resolveToken(opts.asset, chainKey);
55790
- if (tokenInfo) {
55791
- const swapClient = new SwapClient(httpClient);
55792
- const price = await swapClient.getTokenPrice(chainKey, tokenInfo.address);
55793
- if (price && price > 0) {
55794
- const usdValue = parsedAmount * price;
55795
- if (usdValue > maxUsd) {
55796
- throw new TransferError(
55797
- "USD_LIMIT_EXCEEDED",
55798
- `Payout value ~$${usdValue.toFixed(2)} USD exceeds safety limit of $${maxUsd}. Re-run with --max-usd ${Math.ceil(usdValue)} or --skip-safety-check to proceed.`
55799
- );
55800
- }
55801
- if (usdValue > 1e3) {
55802
- warn(`High-value off-ramp payout: ~$${usdValue.toFixed(2)} USD`);
55803
- }
55804
- }
55805
- }
55806
- } catch (err) {
55807
- if (err instanceof CommandError) throw err;
55808
- warn(
55809
- "Could not verify payout USD value (price lookup failed). Use --skip-safety-check to suppress."
55810
- );
55811
- }
55884
+ safety = { maxUsd };
55812
55885
  }
55813
55886
  const address = await provider.getAddress(chainKey);
55814
55887
  warn(
55815
55888
  `About to send ${opts.amount} ${opts.asset} from ${address} to third-party off-ramp deposit address ${opts.to}${opts.memo ? ` (memo: ${opts.memo})` : ""}. This address is not under your control \u2014 verify it matches what the provider displayed before continuing.`
55816
55889
  );
55817
- let result;
55818
- try {
55819
- result = await provider.transfer({
55820
- assetId: opts.asset,
55821
- to: opts.to,
55822
- amount: opts.amount,
55823
- decimals,
55824
- memo: opts.memo,
55825
- // Mainnet by construction — onramp has no --chain input and derives
55826
- // chainKey from the asset's coinId via mainnetChainKeyForCoinId.
55827
- chainKey
55828
- });
55829
- } catch (err) {
55830
- mapBroadcastError(err);
55890
+ const { contractAddress } = parseTokenId(opts.asset);
55891
+ const executor = new TransferExecutor({
55892
+ wallet: provider,
55893
+ swapClient: new SwapClient(await getClient()),
55894
+ onInfo: info,
55895
+ onWarn: warn
55896
+ });
55897
+ const result = await executor.execute({
55898
+ // Mainnet by construction — onramp has no --chain input and derives
55899
+ // chainKey from the asset's coinId via mainnetChainKeyForCoinId.
55900
+ chain: chainKey,
55901
+ to: opts.to,
55902
+ resolveNames: false,
55903
+ amount: opts.amount,
55904
+ tokenAddress: contractAddress,
55905
+ decimals: opts.decimals,
55906
+ memo: opts.memo,
55907
+ safety
55908
+ });
55909
+ if (!result.success) {
55910
+ throw new CommandError(result.code, result.message);
55831
55911
  }
55832
- const explorerUrl = getExplorerTxUrl(result.chainKey, result.hash);
55833
55912
  success("Off-ramp payout broadcast!");
55834
55913
  if (!opts.json) {
55835
55914
  info(
@@ -55840,13 +55919,13 @@ ${url}`);
55840
55919
  {
55841
55920
  hash: result.hash,
55842
55921
  chain: result.chainKey,
55843
- from: address,
55844
- to: opts.to,
55922
+ from: result.from,
55923
+ to: result.to,
55845
55924
  amount: opts.amount,
55846
55925
  asset: opts.asset,
55847
55926
  memo: opts.memo,
55848
55927
  quoteId: opts.quoteId,
55849
- explorer: explorerUrl
55928
+ explorer: result.explorerUrl
55850
55929
  },
55851
55930
  opts.json
55852
55931
  );
@@ -56319,7 +56398,7 @@ init_dist();
56319
56398
  // package.json
56320
56399
  var package_default = {
56321
56400
  name: "@trustwallet/cli",
56322
- version: "0.24.2",
56401
+ version: "0.25.0",
56323
56402
  description: "TWAK CLI \u2014 command-line interface for Trust Wallet Agent Kit",
56324
56403
  main: "dist/index.js",
56325
56404
  types: "dist/index.d.ts",