@trustwallet/cli 0.10.0 → 0.11.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
@@ -3111,9 +3111,12 @@ var require_commander = __commonJS({
3111
3111
  var dist_exports = {};
3112
3112
  __export(dist_exports, {
3113
3113
  ALERT_ERROR_CODES: () => ALERT_ERROR_CODES,
3114
+ ALLOWED_OFFRAMP_PROVIDERS: () => ALLOWED_OFFRAMP_PROVIDERS,
3114
3115
  AUTOMATION_ERROR_CODES: () => AUTOMATION_ERROR_CODES,
3115
3116
  AlertError: () => AlertError,
3116
3117
  AutomationError: () => AutomationError,
3118
+ BALANCE_ERROR_CODES: () => BALANCE_ERROR_CODES,
3119
+ BalanceError: () => BalanceError,
3117
3120
  CELO_NATIVE_ADDRESS: () => CELO_NATIVE_ADDRESS,
3118
3121
  CHAINS: () => CHAINS,
3119
3122
  COMMON_ERROR_CODES: () => COMMON_ERROR_CODES,
@@ -3140,12 +3143,15 @@ __export(dist_exports, {
3140
3143
  WatchError: () => WatchError,
3141
3144
  X402Error: () => X402Error,
3142
3145
  X402_ERROR_CODES: () => X402_ERROR_CODES,
3146
+ assertStrictPositiveAmount: () => assertStrictPositiveAmount,
3143
3147
  chainKeyToCoinType: () => chainKeyToCoinType,
3144
3148
  classifyApiError: () => classifyApiError,
3145
3149
  classifyStorageError: () => classifyStorageError,
3146
3150
  classifyWalletLoadError: () => classifyWalletLoadError,
3147
3151
  coinTypeToChainKey: () => coinTypeToChainKey,
3148
3152
  createTwakClient: () => createTwakClient,
3153
+ fetchBalance: () => fetchBalance,
3154
+ fetchBalances: () => fetchBalances,
3149
3155
  fiatToTokenAmount: () => fiatToTokenAmount,
3150
3156
  formatAmount: () => formatAmount,
3151
3157
  formatTokenId: () => formatTokenId,
@@ -3167,12 +3173,14 @@ __export(dist_exports, {
3167
3173
  parseAmount: () => parseAmount,
3168
3174
  parseHumanAmount: () => parseHumanAmount,
3169
3175
  parseTokenId: () => parseTokenId,
3176
+ rankQuotesByBest: () => rankQuotesByBest,
3170
3177
  resolveAddressOrName: () => resolveAddressOrName,
3171
3178
  resolveName: () => resolveName,
3172
3179
  resolveToken: () => resolveToken,
3173
3180
  signRequest: () => signRequest,
3174
3181
  sortQueryParams: () => sortQueryParams,
3175
3182
  sortQueryParamsGoCompat: () => sortQueryParamsGoCompat,
3183
+ toCamelCase: () => toCamelCase,
3176
3184
  tokenIdToChain: () => tokenIdToChain,
3177
3185
  withRetry: () => withRetry
3178
3186
  });
@@ -3397,10 +3405,10 @@ function sortQueryParamsGoCompat(query) {
3397
3405
  }
3398
3406
  function formatAmount(amount, decimals) {
3399
3407
  const value = BigInt(amount);
3400
- const divisor = BigInt(10 ** decimals);
3408
+ const divisor = 10n ** BigInt(decimals);
3401
3409
  const integerPart = value / divisor;
3402
3410
  const fractionalPart = value % divisor;
3403
- if (fractionalPart === BigInt(0)) return integerPart.toString();
3411
+ if (fractionalPart === 0n) return integerPart.toString();
3404
3412
  const fractionalStr = fractionalPart.toString().padStart(decimals, "0");
3405
3413
  const trimmedFractional = fractionalStr.replace(/0+$/, "");
3406
3414
  return `${integerPart}.${trimmedFractional}`;
@@ -3411,6 +3419,24 @@ function parseAmount(amount, decimals) {
3411
3419
  const combined = integerPart + paddedFractional;
3412
3420
  return BigInt(combined).toString();
3413
3421
  }
3422
+ function assertStrictPositiveAmount(amount, decimals) {
3423
+ if (!DECIMAL_AMOUNT_RE.test(amount)) {
3424
+ throw new Error(
3425
+ `Amount must be a positive base-10 decimal string (no sign, no exponent). Got: ${amount}`
3426
+ );
3427
+ }
3428
+ if (/^0+(?:\.0+)?$/.test(amount)) {
3429
+ throw new Error("Amount must be greater than 0");
3430
+ }
3431
+ if (decimals !== void 0) {
3432
+ const frac = amount.split(".")[1] ?? "";
3433
+ if (frac.length > decimals) {
3434
+ throw new Error(
3435
+ `Amount has ${frac.length} fractional digits but the token has only ${decimals} decimals. Truncating excess precision would under-send and likely invalidate the order.`
3436
+ );
3437
+ }
3438
+ }
3439
+ }
3414
3440
  function parseHumanAmount(amount, decimals) {
3415
3441
  const trimmed = amount.trim();
3416
3442
  if (trimmed.toLowerCase() === "all") return "all";
@@ -3421,10 +3447,24 @@ function parseHumanAmount(amount, decimals) {
3421
3447
  function fiatToTokenAmount(fiatAmount, priceUsd, decimals) {
3422
3448
  const stripped = fiatAmount.trim().replace(/^[$€£¥]/, "").trim();
3423
3449
  const usdValue = parseFloat(stripped);
3424
- if (isNaN(usdValue) || priceUsd <= 0) throw new Error(`Invalid fiat amount or price: ${fiatAmount}`);
3450
+ if (isNaN(usdValue) || priceUsd <= 0)
3451
+ throw new Error(`Invalid fiat amount or price: ${fiatAmount}`);
3425
3452
  const tokenAmount = usdValue / priceUsd;
3426
3453
  return parseAmount(tokenAmount.toFixed(decimals), decimals);
3427
3454
  }
3455
+ function toCamelCase(obj) {
3456
+ if (Array.isArray(obj)) return obj.map(toCamelCase);
3457
+ if (obj !== null && typeof obj === "object") {
3458
+ const result = /* @__PURE__ */ Object.create(null);
3459
+ for (const [key, value] of Object.entries(obj)) {
3460
+ const camelKey = key.replace(/_([a-z])/g, (_2, char) => char.toUpperCase());
3461
+ if (UNSAFE_KEYS.has(key) || UNSAFE_KEYS.has(camelKey)) continue;
3462
+ result[camelKey] = toCamelCase(value);
3463
+ }
3464
+ return result;
3465
+ }
3466
+ return obj;
3467
+ }
3428
3468
  async function resolveName(name2) {
3429
3469
  if (!name2.includes(".")) return null;
3430
3470
  try {
@@ -3513,7 +3553,60 @@ async function withRetry(fn2, opts = {}) {
3513
3553
  }
3514
3554
  }
3515
3555
  }
3516
- var import_crypto, NATIVE_TOKEN_ADDRESS, SOLANA_NATIVE_ADDRESS, CHAINS, COMMON_TOKENS, BSC_18_DECIMAL_TOKENS, NATIVE_SYMBOLS, TOKEN_DECIMALS, CELO_NATIVE_ADDRESS, TwakError, TwakApiError, TwakNetworkError, TwakValidationError, CommandError, COMMON_ERROR_CODES, FS_ERRNO_CODES, SWAP_ERROR_CODES, SwapError, TRANSFER_ERROR_CODES, TransferError, WALLET_ERROR_CODES, WalletError, QUERY_ERROR_CODES, QueryError, ALERT_ERROR_CODES, AlertError, ERC20_ERROR_CODES, Erc20Error, WATCH_ERROR_CODES, WatchError, AUTOMATION_ERROR_CODES, AutomationError, X402_ERROR_CODES, X402Error, NAMING_API, DEFAULT_GATEWAY_URL, DEFAULT_MARKET_URL, DEFAULT_TIMEOUT, TwakHttpClient;
3556
+ function rankQuotesByBest(quotes, getAmount) {
3557
+ const valid = [];
3558
+ for (const q3 of quotes) {
3559
+ const raw = getAmount(q3);
3560
+ if (raw === void 0) continue;
3561
+ const n4 = parseFloat(raw);
3562
+ if (!Number.isFinite(n4) || n4 <= 0) continue;
3563
+ valid.push({ quote: q3, amount: n4 });
3564
+ }
3565
+ if (valid.length === 0) return [];
3566
+ const best = Math.max(...valid.map((v5) => v5.amount));
3567
+ return valid.map(({ quote, amount }) => {
3568
+ const raw = (best - amount) / best * 100;
3569
+ return { ...quote, vsBestPct: Math.round(raw * 100) / 100 };
3570
+ }).sort((a3, b3) => a3.vsBestPct - b3.vsBestPct);
3571
+ }
3572
+ async function fetchBalances(httpClient, assets, currency = "USD") {
3573
+ try {
3574
+ const response = await withRetry(
3575
+ () => httpClient.gateway("POST", "/v1/balances", {
3576
+ currency,
3577
+ addresses: assets.map(({ assetId, address }) => ({
3578
+ asset_id: assetId,
3579
+ address
3580
+ }))
3581
+ })
3582
+ );
3583
+ const parsed = toCamelCase(response);
3584
+ if (!Array.isArray(parsed.balances)) {
3585
+ throw new BalanceError("NETWORK_ERROR", "Could not fetch balances. Please try again later.");
3586
+ }
3587
+ return parsed.balances;
3588
+ } catch (err) {
3589
+ if (err instanceof BalanceError) throw err;
3590
+ throw new BalanceError("NETWORK_ERROR", "Could not fetch balances. Please try again later.");
3591
+ }
3592
+ }
3593
+ async function fetchBalance(httpClient, assetId, address, currency = "USD") {
3594
+ try {
3595
+ const params = new URLSearchParams({ asset_id: assetId, address, currency });
3596
+ const response = await withRetry(
3597
+ () => httpClient.gateway("GET", "/v1/balances", void 0, params.toString())
3598
+ );
3599
+ const parsed = toCamelCase(response);
3600
+ if (!parsed.balance) {
3601
+ throw new BalanceError("NETWORK_ERROR", "Could not fetch balance. Please try again later.");
3602
+ }
3603
+ return parsed.balance;
3604
+ } catch (err) {
3605
+ if (err instanceof BalanceError) throw err;
3606
+ throw new BalanceError("NETWORK_ERROR", "Could not fetch balance. Please try again later.");
3607
+ }
3608
+ }
3609
+ var import_crypto, NATIVE_TOKEN_ADDRESS, SOLANA_NATIVE_ADDRESS, CHAINS, COMMON_TOKENS, BSC_18_DECIMAL_TOKENS, NATIVE_SYMBOLS, TOKEN_DECIMALS, CELO_NATIVE_ADDRESS, TwakError, TwakApiError, TwakNetworkError, TwakValidationError, CommandError, COMMON_ERROR_CODES, FS_ERRNO_CODES, SWAP_ERROR_CODES, SwapError, TRANSFER_ERROR_CODES, TransferError, WALLET_ERROR_CODES, WalletError, QUERY_ERROR_CODES, QueryError, BALANCE_ERROR_CODES, BalanceError, ALERT_ERROR_CODES, AlertError, ERC20_ERROR_CODES, Erc20Error, WATCH_ERROR_CODES, WatchError, AUTOMATION_ERROR_CODES, AutomationError, X402_ERROR_CODES, X402Error, DECIMAL_AMOUNT_RE, UNSAFE_KEYS, NAMING_API, DEFAULT_GATEWAY_URL, DEFAULT_MARKET_URL, DEFAULT_TIMEOUT, DEFAULT_USER_AGENT, DEFAULT_X_ORIGIN, TwakHttpClient, ALLOWED_OFFRAMP_PROVIDERS;
3517
3610
  var init_dist = __esm({
3518
3611
  "../core/dist/index.mjs"() {
3519
3612
  "use strict";
@@ -4098,6 +4191,15 @@ var init_dist = __esm({
4098
4191
  this.name = "QueryError";
4099
4192
  }
4100
4193
  };
4194
+ BALANCE_ERROR_CODES = {
4195
+ ...COMMON_ERROR_CODES
4196
+ };
4197
+ BalanceError = class extends CommandError {
4198
+ constructor(code2, message) {
4199
+ super(code2, message);
4200
+ this.name = "BalanceError";
4201
+ }
4202
+ };
4101
4203
  ALERT_ERROR_CODES = {
4102
4204
  ...COMMON_ERROR_CODES,
4103
4205
  ALERT_NOT_FOUND: "ALERT_NOT_FOUND"
@@ -4148,10 +4250,14 @@ var init_dist = __esm({
4148
4250
  this.name = "X402Error";
4149
4251
  }
4150
4252
  };
4253
+ DECIMAL_AMOUNT_RE = /^(?:0|[1-9]\d*)(?:\.\d+)?$/;
4254
+ UNSAFE_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
4151
4255
  NAMING_API = "https://naming.trustwallet.com/v1/lookup";
4152
4256
  DEFAULT_GATEWAY_URL = "https://tws.trustwallet.com";
4153
4257
  DEFAULT_MARKET_URL = "https://market.trustwallet.com";
4154
4258
  DEFAULT_TIMEOUT = 3e4;
4259
+ DEFAULT_USER_AGENT = "Mozilla/5.0 AgentKit";
4260
+ DEFAULT_X_ORIGIN = "Trust/2.15.0 AgentKit";
4155
4261
  TwakHttpClient = class {
4156
4262
  config;
4157
4263
  constructor(config2) {
@@ -4159,7 +4265,8 @@ var init_dist = __esm({
4159
4265
  gatewayBaseUrl: DEFAULT_GATEWAY_URL,
4160
4266
  marketBaseUrl: DEFAULT_MARKET_URL,
4161
4267
  timeout: DEFAULT_TIMEOUT,
4162
- userAgent: void 0,
4268
+ userAgent: DEFAULT_USER_AGENT,
4269
+ xOrigin: DEFAULT_X_ORIGIN,
4163
4270
  ...config2
4164
4271
  };
4165
4272
  }
@@ -4218,8 +4325,13 @@ var init_dist = __esm({
4218
4325
  async fetch(url, init3) {
4219
4326
  const controller = new AbortController();
4220
4327
  const timeout = setTimeout(() => controller.abort(), this.config.timeout);
4328
+ const headers = {
4329
+ ...this.config.userAgent ? { "User-Agent": this.config.userAgent } : {},
4330
+ ...this.config.xOrigin ? { "X-Origin": this.config.xOrigin } : {},
4331
+ ...init3.headers
4332
+ };
4221
4333
  try {
4222
- const response = await fetch(url, { ...init3, signal: controller.signal });
4334
+ const response = await fetch(url, { ...init3, headers, signal: controller.signal });
4223
4335
  if (!response.ok) {
4224
4336
  const text = await response.text().catch(() => "");
4225
4337
  let parsed;
@@ -4237,10 +4349,7 @@ var init_dist = __esm({
4237
4349
  return await response.json();
4238
4350
  } catch (error3) {
4239
4351
  if (error3 instanceof TwakApiError) throw error3;
4240
- throw new TwakNetworkError(
4241
- error3 instanceof Error ? error3.message : "Network error",
4242
- error3
4243
- );
4352
+ throw new TwakNetworkError(error3 instanceof Error ? error3.message : "Network error", error3);
4244
4353
  } finally {
4245
4354
  clearTimeout(timeout);
4246
4355
  }
@@ -4252,6 +4361,7 @@ var init_dist = __esm({
4252
4361
  return this.config.hmacSecret;
4253
4362
  }
4254
4363
  };
4364
+ ALLOWED_OFFRAMP_PROVIDERS = /* @__PURE__ */ new Set(["MoonPay"]);
4255
4365
  }
4256
4366
  });
4257
4367
 
@@ -5954,14 +6064,14 @@ var require_templates = __commonJS({
5954
6064
  }
5955
6065
  return results;
5956
6066
  }
5957
- function buildStyle(chalk7, styles) {
6067
+ function buildStyle(chalk8, styles) {
5958
6068
  const enabled = {};
5959
6069
  for (const layer of styles) {
5960
6070
  for (const style of layer.styles) {
5961
6071
  enabled[style[0]] = layer.inverse ? null : style.slice(1);
5962
6072
  }
5963
6073
  }
5964
- let current = chalk7;
6074
+ let current = chalk8;
5965
6075
  for (const [styleName, styles2] of Object.entries(enabled)) {
5966
6076
  if (!Array.isArray(styles2)) {
5967
6077
  continue;
@@ -5973,7 +6083,7 @@ var require_templates = __commonJS({
5973
6083
  }
5974
6084
  return current;
5975
6085
  }
5976
- module2.exports = (chalk7, temporary) => {
6086
+ module2.exports = (chalk8, temporary) => {
5977
6087
  const styles = [];
5978
6088
  const chunks = [];
5979
6089
  let chunk2 = [];
@@ -5983,13 +6093,13 @@ var require_templates = __commonJS({
5983
6093
  } else if (style) {
5984
6094
  const string4 = chunk2.join("");
5985
6095
  chunk2 = [];
5986
- chunks.push(styles.length === 0 ? string4 : buildStyle(chalk7, styles)(string4));
6096
+ chunks.push(styles.length === 0 ? string4 : buildStyle(chalk8, styles)(string4));
5987
6097
  styles.push({ inverse, styles: parseStyle(style) });
5988
6098
  } else if (close) {
5989
6099
  if (styles.length === 0) {
5990
6100
  throw new Error("Found extraneous } in Chalk template literal");
5991
6101
  }
5992
- chunks.push(buildStyle(chalk7, styles)(chunk2.join("")));
6102
+ chunks.push(buildStyle(chalk8, styles)(chunk2.join("")));
5993
6103
  chunk2 = [];
5994
6104
  styles.pop();
5995
6105
  } else {
@@ -6037,16 +6147,16 @@ var require_source = __commonJS({
6037
6147
  }
6038
6148
  };
6039
6149
  var chalkFactory = (options) => {
6040
- const chalk8 = {};
6041
- applyOptions(chalk8, options);
6042
- chalk8.template = (...arguments_) => chalkTag(chalk8.template, ...arguments_);
6043
- Object.setPrototypeOf(chalk8, Chalk.prototype);
6044
- Object.setPrototypeOf(chalk8.template, chalk8);
6045
- chalk8.template.constructor = () => {
6150
+ const chalk9 = {};
6151
+ applyOptions(chalk9, options);
6152
+ chalk9.template = (...arguments_) => chalkTag(chalk9.template, ...arguments_);
6153
+ Object.setPrototypeOf(chalk9, Chalk.prototype);
6154
+ Object.setPrototypeOf(chalk9.template, chalk9);
6155
+ chalk9.template.constructor = () => {
6046
6156
  throw new Error("`chalk.constructor()` is deprecated. Use `new chalk.Instance()` instead.");
6047
6157
  };
6048
- chalk8.template.Instance = ChalkClass;
6049
- return chalk8.template;
6158
+ chalk9.template.Instance = ChalkClass;
6159
+ return chalk9.template;
6050
6160
  };
6051
6161
  function Chalk(options) {
6052
6162
  return chalkFactory(options);
@@ -6157,7 +6267,7 @@ var require_source = __commonJS({
6157
6267
  return openAll + string4 + closeAll;
6158
6268
  };
6159
6269
  var template;
6160
- var chalkTag = (chalk8, ...strings) => {
6270
+ var chalkTag = (chalk9, ...strings) => {
6161
6271
  const [firstString] = strings;
6162
6272
  if (!isArray(firstString) || !isArray(firstString.raw)) {
6163
6273
  return strings.join(" ");
@@ -6173,14 +6283,14 @@ var require_source = __commonJS({
6173
6283
  if (template === void 0) {
6174
6284
  template = require_templates();
6175
6285
  }
6176
- return template(chalk8, parts.join(""));
6286
+ return template(chalk9, parts.join(""));
6177
6287
  };
6178
6288
  Object.defineProperties(Chalk.prototype, styles);
6179
- var chalk7 = Chalk();
6180
- chalk7.supportsColor = stdoutColor;
6181
- chalk7.stderr = Chalk({ level: stderrColor ? stderrColor.level : 0 });
6182
- chalk7.stderr.supportsColor = stderrColor;
6183
- module2.exports = chalk7;
6289
+ var chalk8 = Chalk();
6290
+ chalk8.supportsColor = stdoutColor;
6291
+ chalk8.stderr = Chalk({ level: stderrColor ? stderrColor.level : 0 });
6292
+ chalk8.stderr.supportsColor = stderrColor;
6293
+ module2.exports = chalk8;
6184
6294
  }
6185
6295
  });
6186
6296
 
@@ -12876,7 +12986,7 @@ var require_boxen = __commonJS({
12876
12986
  "../../node_modules/.pnpm/boxen@5.1.2/node_modules/boxen/index.js"(exports2, module2) {
12877
12987
  "use strict";
12878
12988
  var stringWidth = require_string_width();
12879
- var chalk7 = require_source();
12989
+ var chalk8 = require_source();
12880
12990
  var widestLine = require_widest_line();
12881
12991
  var cliBoxes = require_cli_boxes();
12882
12992
  var camelCase = require_camelcase();
@@ -13019,9 +13129,9 @@ var require_boxen = __commonJS({
13019
13129
  return lines.join(NL);
13020
13130
  };
13021
13131
  var isHex = (color) => color.match(/^#(?:[0-f]{3}){1,2}$/i);
13022
- var isColorValid = (color) => typeof color === "string" && (chalk7[color] || isHex(color));
13023
- var getColorFn = (color) => isHex(color) ? chalk7.hex(color) : chalk7[color];
13024
- var getBGColorFn = (color) => isHex(color) ? chalk7.bgHex(color) : chalk7[camelCase(["bg", color])];
13132
+ var isColorValid = (color) => typeof color === "string" && (chalk8[color] || isHex(color));
13133
+ var getColorFn = (color) => isHex(color) ? chalk8.hex(color) : chalk8[color];
13134
+ var getBGColorFn = (color) => isHex(color) ? chalk8.bgHex(color) : chalk8[camelCase(["bg", color])];
13025
13135
  module2.exports = (text, options) => {
13026
13136
  options = {
13027
13137
  padding: 0,
@@ -13047,7 +13157,7 @@ var require_boxen = __commonJS({
13047
13157
  const margin = getObject(options.margin);
13048
13158
  const colorizeBorder = (border) => {
13049
13159
  const newBorder = options.borderColor ? getColorFn(options.borderColor)(border) : border;
13050
- return options.dimBorder ? chalk7.dim(newBorder) : newBorder;
13160
+ return options.dimBorder ? chalk8.dim(newBorder) : newBorder;
13051
13161
  };
13052
13162
  const colorizeContent = (content) => options.backgroundColor ? getBGColorFn(options.backgroundColor)(content) : content;
13053
13163
  const columns = terminalColumns();
@@ -13090,242 +13200,6 @@ var require_boxen = __commonJS({
13090
13200
  }
13091
13201
  });
13092
13202
 
13093
- // ../security/dist/index.mjs
13094
- var dist_exports3 = {};
13095
- __export(dist_exports3, {
13096
- SecurityService: () => SecurityService
13097
- });
13098
- var SecurityService;
13099
- var init_dist5 = __esm({
13100
- "../security/dist/index.mjs"() {
13101
- "use strict";
13102
- init_dist();
13103
- SecurityService = class {
13104
- constructor(client) {
13105
- this.client = client;
13106
- }
13107
- async validate(params) {
13108
- const query = new URLSearchParams();
13109
- if (params.assetId) query.set("asset_id", params.assetId);
13110
- query.set("address", params.address);
13111
- if (params.type) query.set("type", params.type);
13112
- if (params.version) query.set("version", params.version.toString());
13113
- try {
13114
- return await this.client.gateway(
13115
- "GET",
13116
- "/v1/validate",
13117
- void 0,
13118
- query.toString()
13119
- );
13120
- } catch (err) {
13121
- const msg = err instanceof Error ? err.message : String(err);
13122
- const details = err?.details;
13123
- let detailsStr = "";
13124
- if (details != null) {
13125
- try {
13126
- detailsStr = JSON.stringify(details);
13127
- } catch {
13128
- try {
13129
- detailsStr = String(details);
13130
- } catch {
13131
- }
13132
- }
13133
- }
13134
- if (msg.includes("GraphQL") || msg.includes("Failed to decode") || detailsStr.includes("GraphQL") || detailsStr.includes("Failed to decode")) {
13135
- return { valid: false, message: "Invalid address format" };
13136
- }
13137
- throw err;
13138
- }
13139
- }
13140
- async getAssetInfo(assetId) {
13141
- const info2 = await this.client.gateway(
13142
- "GET",
13143
- `/v1/assets/${encodeURIComponent(assetId)}`
13144
- );
13145
- try {
13146
- const priceResp = await this.client.market("POST", "/v2/market/tickers", { currency: "USD", assets: [assetId] });
13147
- const ticker = priceResp.tickers?.[0];
13148
- if (ticker) {
13149
- if (ticker.price != null) info2.priceUsd = ticker.price;
13150
- if (ticker.change_24h != null) info2.priceChange24h = ticker.change_24h;
13151
- if (ticker.market_cap != null) info2.marketCapUsd = ticker.market_cap;
13152
- if (ticker.volume_24h != null) info2.volume24hUsd = ticker.volume_24h;
13153
- }
13154
- } catch {
13155
- }
13156
- return info2;
13157
- }
13158
- async getTokenHoldings(assets, fromTime = 0) {
13159
- const response = await this.client.gateway("POST", "/v1/assets", {
13160
- from_time: fromTime,
13161
- version: 2,
13162
- assets
13163
- });
13164
- return Array.isArray(response) ? response : response.docs ?? response.data ?? [];
13165
- }
13166
- async checkCoinStatus(assetId) {
13167
- const query = new URLSearchParams({
13168
- version: "8.4",
13169
- platform: "android",
13170
- include_security_info: "true",
13171
- include_solana_security_info: "true"
13172
- });
13173
- const raw = await this.client.gateway(
13174
- "GET",
13175
- `/v2/coinstatus/${encodeURIComponent(assetId)}`,
13176
- void 0,
13177
- query.toString()
13178
- );
13179
- return this.transformCoinStatus(assetId, raw);
13180
- }
13181
- async getAssetListings(params) {
13182
- const limit = params.limit || 20;
13183
- const query = new URLSearchParams({
13184
- version: "27",
13185
- currency: params.currency || "USD",
13186
- limit: String(params.sort ? Math.max(limit, 50) : limit)
13187
- });
13188
- if (params.categoryId) query.set("category_id", params.categoryId);
13189
- if (params.networks) query.set("networks", params.networks);
13190
- const response = await this.client.gateway(
13191
- "GET",
13192
- "/v1/assets/listings",
13193
- void 0,
13194
- query.toString()
13195
- );
13196
- const docs = response.docs || response.results || [];
13197
- const tokens = docs.map((doc) => {
13198
- const asset = doc.asset || doc;
13199
- const price = doc.price || {};
13200
- const market = doc.market || {};
13201
- const assetIdRaw = asset.asset_id || doc.asset_id || "";
13202
- const coinId = assetIdRaw ? parseInt(assetIdRaw.replace(/^c/, "").split("_")[0], 10) : NaN;
13203
- const chain2 = !isNaN(coinId) ? coinTypeToChainKey(coinId) || "" : "";
13204
- return {
13205
- assetId: assetIdRaw,
13206
- name: asset.name || doc.name || "",
13207
- symbol: asset.symbol || doc.symbol || "",
13208
- chain: chain2,
13209
- logoUrl: asset.icon_url || doc.icon_url,
13210
- priceUsd: price.price ?? doc.price_usd,
13211
- priceChange24h: price.percent_change_24h ?? doc.price_change_24h,
13212
- marketCapUsd: market.market_cap ?? price.market_cap ?? doc.market_cap_usd,
13213
- volume24hUsd: market.volume_24h ?? price.volume_24h ?? doc.volume_24h_usd
13214
- };
13215
- });
13216
- if (params.sort) {
13217
- const sortKeyMap = {
13218
- percent_change_24h: "priceChange24h",
13219
- market_cap: "marketCapUsd",
13220
- volume_24h: "volume24hUsd"
13221
- };
13222
- const key = sortKeyMap[params.sort] || "priceChange24h";
13223
- tokens.sort((a3, b3) => (b3[key] ?? 0) - (a3[key] ?? 0));
13224
- }
13225
- return tokens.slice(0, limit);
13226
- }
13227
- async getTrendingTokens(limit = 20, sort = "percent_change_24h") {
13228
- const knownAssets = [
13229
- "c60",
13230
- "c501",
13231
- "c0",
13232
- "c20000714",
13233
- "c966",
13234
- "c10009000",
13235
- "c8453",
13236
- "c10042221",
13237
- "c10000070",
13238
- "c195",
13239
- "c637",
13240
- "c607",
13241
- "c784",
13242
- "c397",
13243
- "c118",
13244
- "c2",
13245
- "c3",
13246
- "c10000250",
13247
- "c10000324",
13248
- "c81457"
13249
- ];
13250
- const response = await this.client.market("POST", "/v2/market/tickers", {
13251
- currency: "USD",
13252
- assets: knownAssets
13253
- });
13254
- const tickers = response.tickers || [];
13255
- const sortKey = sort === "market_cap" ? "market_cap" : sort === "volume_24h" ? "volume_24h" : "change_24h";
13256
- return tickers.sort((a3, b3) => (b3[sortKey] ?? 0) - (a3[sortKey] ?? 0)).slice(0, limit).map((t) => {
13257
- const coinId = t.id ? parseInt(t.id.replace(/^c/, ""), 10) : NaN;
13258
- const chain2 = !isNaN(coinId) ? coinTypeToChainKey(coinId) || "" : "";
13259
- return {
13260
- assetId: t.id || "",
13261
- name: t.slug || t.id || "",
13262
- symbol: t.slug?.toUpperCase() || "",
13263
- chain: chain2,
13264
- logoUrl: void 0,
13265
- priceUsd: t.price,
13266
- priceChange24h: t.change_24h,
13267
- marketCapUsd: t.market_cap,
13268
- volume24hUsd: t.volume_24h
13269
- };
13270
- });
13271
- }
13272
- transformCoinStatus(assetId, raw) {
13273
- if (!raw) return { assetId, isActive: false };
13274
- const asset = raw.asset || raw;
13275
- const security = raw.security_info || raw.securityInfo;
13276
- const solanaSecurity = raw.solana_security_info || raw.solanaSecurityInfo;
13277
- const warnings = [];
13278
- let isHoneypot;
13279
- if (security) {
13280
- const allItems = [
13281
- ...security.contract_security?.items || [],
13282
- ...security.honeypot_risk?.items || []
13283
- ];
13284
- for (const item of allItems) {
13285
- if (item.type === "warning" || item.type === "risk") {
13286
- const message = item.summary ?? item.description ?? item.code;
13287
- if (message) warnings.push(message);
13288
- }
13289
- if (item.code === "is_honeypot" && item.type === "risk") isHoneypot = true;
13290
- }
13291
- }
13292
- const totalRisks = (security?.contract_security?.num_risks ?? 0) + (security?.honeypot_risk?.num_risks ?? 0);
13293
- const totalWarnings = (security?.contract_security?.num_warnings ?? 0) + (security?.honeypot_risk?.num_warnings ?? 0);
13294
- const computedRiskLevel = totalRisks > 0 ? "high" : totalWarnings > 0 ? "medium" : security ? "low" : void 0;
13295
- let finalRiskLevel = security?.risk_level ?? security?.riskLevel ?? computedRiskLevel;
13296
- const knownStablecoins = ["USDC", "USDT", "DAI", "BUSD", "TUSD", "USDP", "FRAX", "PYUSD"];
13297
- const symbol = (asset.symbol ?? raw.symbol ?? "").toUpperCase();
13298
- let riskContext;
13299
- if (finalRiskLevel === "medium" && knownStablecoins.includes(symbol) && totalRisks === 0) {
13300
- riskContext = "Warnings are due to proxy contract pattern, common in audited stablecoins";
13301
- finalRiskLevel = "low";
13302
- }
13303
- return {
13304
- assetId,
13305
- name: asset.name ?? raw.name,
13306
- symbol: asset.symbol ?? raw.symbol,
13307
- isActive: raw.is_active ?? raw.isActive ?? true,
13308
- supportsSwap: raw.isSwapEnabled ?? raw.supports_swap ?? raw.supportsSwap,
13309
- supportsStaking: raw.isStakeEnabled ?? raw.supports_staking ?? raw.supportsStaking,
13310
- securityInfo: security ? {
13311
- isHoneypot: isHoneypot ?? security.is_honeypot ?? security.isHoneypot,
13312
- hasAudit: security.has_audit ?? security.hasAudit ?? security.is_scanned,
13313
- auditProvider: security.audit_provider ?? security.auditProvider ?? security.provider,
13314
- riskLevel: finalRiskLevel,
13315
- ...riskContext ? { riskContext } : {},
13316
- warnings: warnings.length > 0 ? warnings : security.warnings
13317
- } : void 0,
13318
- solanaSecurityInfo: solanaSecurity ? {
13319
- isMintable: solanaSecurity.is_mintable ?? solanaSecurity.isMintable,
13320
- hasFreezeAuthority: solanaSecurity.has_freeze_authority ?? solanaSecurity.hasFreezeAuthority,
13321
- riskLevel: solanaSecurity.risk_level ?? solanaSecurity.riskLevel
13322
- } : void 0
13323
- };
13324
- }
13325
- };
13326
- }
13327
- });
13328
-
13329
13203
  // ../security/dist/services/dapps-service.mjs
13330
13204
  var dapps_service_exports = {};
13331
13205
  __export(dapps_service_exports, {
@@ -39328,201 +39202,136 @@ ${qr3}`;
39328
39202
  if (!opts.all && !opts.chain) {
39329
39203
  throw new WalletError("VALIDATION_ERROR", "Provide --chain <key> or --all");
39330
39204
  }
39331
- const creds = await ensureTrustSDK();
39205
+ await ensureTrustSDK();
39332
39206
  const password = await resolvePassword(opts);
39333
39207
  const provider = await AgentWalletProvider.fromStorage(password);
39334
- const blockchainClient = provider.getBlockchainClient();
39335
- const fmtRaw = (raw, decimals) => {
39336
- if (!raw) return void 0;
39337
- const big = BigInt(raw);
39338
- const div = 10n ** BigInt(decimals);
39339
- return `${big / div}.${(big % div).toString().padStart(decimals, "0").slice(0, 6)}`;
39340
- };
39341
- const holdingsMap = /* @__PURE__ */ new Map();
39342
- try {
39343
- const { SecurityService: SecurityService2 } = await Promise.resolve().then(() => (init_dist5(), dist_exports3));
39344
- const httpClient = await getClient();
39345
- const securityService = new SecurityService2(httpClient);
39346
- const allAddrs = await provider.getAddresses();
39347
- const assets = allAddrs.map((a3) => {
39348
- const ch = getChainById(a3.chainId);
39349
- return ch ? { coin: ch.coinId, address: a3.address } : null;
39350
- }).filter((a3) => a3 !== null);
39351
- const holdings = await securityService.getTokenHoldings(assets);
39352
- for (const h4 of holdings) {
39353
- if (h4.tokens?.length) {
39354
- holdingsMap.set(
39355
- h4.coin,
39356
- h4.tokens.map((t) => ({
39357
- symbol: t.symbol ?? t.contract,
39358
- contract: t.contract,
39359
- decimals: t.decimals ?? 18
39360
- }))
39361
- );
39362
- }
39208
+ const httpClient = await getClient();
39209
+ if (!opts.all) {
39210
+ const chain2 = getChainById(opts.chain);
39211
+ if (!chain2) {
39212
+ throw new WalletError("CHAIN_UNSUPPORTED", `Unknown chain: ${opts.chain}`);
39363
39213
  }
39364
- } catch {
39365
39214
  }
39366
- if (opts.all && holdingsMap.size === 0) {
39367
- try {
39368
- await registerWalletWithBackend(creds, provider);
39369
- const { SecurityService: Sec } = await Promise.resolve().then(() => (init_dist5(), dist_exports3));
39370
- const retryClient = await getClient();
39371
- const retrySec = new Sec(retryClient);
39372
- const retryAddrs = await provider.getAddresses();
39373
- const retryAssets = retryAddrs.map((a3) => {
39374
- const ch = getChainById(a3.chainId);
39375
- return ch ? { coin: ch.coinId, address: a3.address } : null;
39376
- }).filter((a3) => a3 !== null);
39377
- const retryHoldings = await retrySec.getTokenHoldings(retryAssets);
39378
- for (const h4 of retryHoldings) {
39379
- if (h4.tokens?.length) {
39380
- holdingsMap.set(
39381
- h4.coin,
39382
- h4.tokens.map((t) => ({
39383
- symbol: t.symbol ?? t.contract,
39384
- contract: t.contract,
39385
- decimals: t.decimals ?? 18
39386
- }))
39387
- );
39388
- }
39389
- }
39390
- } catch {
39391
- }
39215
+ const allAddresses = await provider.getAddresses();
39216
+ const addressMap = /* @__PURE__ */ new Map();
39217
+ for (const a3 of allAddresses) addressMap.set(a3.chainId, a3.address);
39218
+ const requestedChains = opts.all ? Object.keys(CHAINS) : [opts.chain];
39219
+ const skipTokens = opts.tokens === false;
39220
+ const chainEntries = requestedChains.flatMap((chainKey) => {
39221
+ const chain2 = getChainById(chainKey);
39222
+ const address = addressMap.get(chainKey);
39223
+ if (!chain2 || !address) return [];
39224
+ return [{ chainKey, chain: chain2, address }];
39225
+ });
39226
+ if (!opts.all && chainEntries.length === 0) {
39227
+ throw new WalletError(
39228
+ "CHAIN_UNSUPPORTED",
39229
+ `Wallet does not have an address for chain: ${opts.chain}`
39230
+ );
39392
39231
  }
39393
- const fetchChainBalance = async (chainKey, chain2, address, skipTokens) => {
39394
- const fmt = (raw) => fmtRaw(raw, chain2.decimals);
39395
- const nativeAssetId = `c${chain2.coinId}`;
39396
- const nativeBal = await blockchainClient.getBalance(nativeAssetId, address);
39397
- const tokens = [];
39398
- if (!skipTokens) {
39399
- const held = holdingsMap.get(chain2.coinId);
39400
- const tokenDefs = [];
39401
- if (held && held.length > 0) {
39402
- tokenDefs.push(...held);
39403
- } else {
39404
- const knownTokens = COMMON_TOKENS[chainKey];
39405
- if (knownTokens) {
39406
- for (const [sym, addr] of Object.entries(knownTokens)) {
39407
- const resolved = resolveToken(sym, chainKey);
39408
- if (!resolved) continue;
39409
- tokenDefs.push({ symbol: sym, contract: addr, decimals: resolved.decimals });
39410
- }
39232
+ const nativeAssets = chainEntries.map(({ chainKey, chain: chain2, address }) => ({
39233
+ chainKey,
39234
+ chain: chain2,
39235
+ token: void 0,
39236
+ assetId: `c${chain2.coinId}`,
39237
+ userAddress: address
39238
+ }));
39239
+ const tokenAssets = skipTokens ? [] : chainEntries.flatMap(({ chainKey, chain: chain2, address }) => {
39240
+ const knownTokens = COMMON_TOKENS[chainKey];
39241
+ if (!knownTokens) return [];
39242
+ return Object.keys(knownTokens).flatMap((sym) => {
39243
+ const resolved = resolveToken(sym, chainKey);
39244
+ if (!resolved) return [];
39245
+ return [
39246
+ {
39247
+ chainKey,
39248
+ chain: chain2,
39249
+ token: { ...resolved, symbol: sym },
39250
+ assetId: `c${chain2.coinId}_t${resolved.address}`,
39251
+ userAddress: address
39411
39252
  }
39412
- }
39413
- const tokenResults = await Promise.all(
39414
- tokenDefs.map(async (t) => {
39415
- try {
39416
- const bal = await blockchainClient.getBalance(
39417
- `c${chain2.coinId}_t${t.contract}`,
39418
- address
39419
- );
39420
- const rawBal = bal.total ?? bal.available ?? "0";
39421
- if (BigInt(rawBal) <= 0n) return null;
39422
- const human = fmtRaw(rawBal, t.decimals);
39423
- if (human && human !== "0.000000")
39424
- return { symbol: t.symbol, contract: t.contract, balance: human };
39425
- } catch {
39426
- }
39427
- return null;
39428
- })
39429
- );
39430
- for (const t of tokenResults) {
39431
- if (t) tokens.push(t);
39432
- }
39433
- }
39434
- return {
39253
+ ];
39254
+ });
39255
+ });
39256
+ const allAssets = [...nativeAssets, ...tokenAssets];
39257
+ const balances = await fetchBalances(
39258
+ httpClient,
39259
+ allAssets.map(({ assetId, userAddress }) => ({ assetId, address: userAddress }))
39260
+ );
39261
+ const assetsById = new Map(allAssets.map((a3) => [a3.assetId, a3]));
39262
+ const resultsByChain = /* @__PURE__ */ new Map();
39263
+ for (const { chainKey, chain: chain2, address } of chainEntries) {
39264
+ resultsByChain.set(chainKey, {
39435
39265
  chain: chainKey,
39436
39266
  address,
39437
39267
  symbol: chain2.symbol,
39438
- available: fmt(nativeBal.available),
39439
- ...nativeBal.staked ? { staked: fmt(nativeBal.staked) } : {},
39440
- ...nativeBal.pending ? { pending: fmt(nativeBal.pending) } : {},
39441
- total: fmt(nativeBal.total),
39442
- tokens
39443
- };
39444
- };
39445
- if (opts.all) {
39446
- const chainKeys = Object.keys(CHAINS);
39447
- const allAddresses = await provider.getAddresses();
39448
- const addressMap = /* @__PURE__ */ new Map();
39449
- for (const a3 of allAddresses) {
39450
- addressMap.set(a3.chainId, a3.address);
39268
+ available: "0",
39269
+ total: "0",
39270
+ totalUsd: null,
39271
+ tokens: []
39272
+ });
39273
+ }
39274
+ for (const balance of balances) {
39275
+ const assetInfo = assetsById.get(balance.assetId);
39276
+ if (!assetInfo) {
39277
+ throw new WalletError(
39278
+ "NETWORK_ERROR",
39279
+ "Could not fetch balances. Please try again later."
39280
+ );
39451
39281
  }
39452
- const results = [];
39453
- const chainEntries = chainKeys.map((k4) => ({ chainKey: k4, chain: getChainById(k4), address: addressMap.get(k4) })).filter(
39454
- (e2) => !!e2.chain && !!e2.address
39455
- );
39456
- const settled = await Promise.allSettled(
39457
- chainEntries.map(
39458
- async ({ chainKey, chain: chain2, address }) => fetchChainBalance(chainKey, chain2, address, opts.tokens === false)
39459
- )
39460
- );
39461
- for (let i2 = 0; i2 < settled.length; i2++) {
39462
- const result = settled[i2];
39463
- if (result.status === "rejected") {
39464
- const chainKey = chainEntries[i2].chainKey;
39465
- results.push({
39466
- chain: chainKey,
39467
- error: result.reason instanceof Error ? result.reason.message : String(result.reason)
39282
+ const result = resultsByChain.get(assetInfo.chainKey);
39283
+ if (!result) continue;
39284
+ const { token, chain: chain2 } = assetInfo;
39285
+ const amts = balance.amounts;
39286
+ if (token) {
39287
+ const human = formatAmount(amts.total ?? "0", token.decimals);
39288
+ if (human !== "0") {
39289
+ result.tokens.push({
39290
+ symbol: token.symbol,
39291
+ contract: token.address,
39292
+ balance: human
39468
39293
  });
39469
- continue;
39470
39294
  }
39471
- const r3 = result.value;
39472
- const rawTotal = r3.total;
39473
- const hasNative = rawTotal && rawTotal !== "0.000000" && !rawTotal.startsWith("0.000000");
39474
- const hasTokens = r3.tokens.length > 0;
39475
- if (hasNative || hasTokens) {
39476
- results.push(r3);
39295
+ } else {
39296
+ result.available = formatAmount(amts.available ?? "0", chain2.decimals);
39297
+ result.total = formatAmount(amts.total ?? "0", chain2.decimals);
39298
+ const lockedHuman = formatAmount(amts.locked ?? "0", chain2.decimals);
39299
+ if (lockedHuman !== "0") result.staked = lockedHuman;
39300
+ const fiat = amts.totalInFiat;
39301
+ result.totalUsd = fiat && fiat !== "" ? parseFloat(fiat) : null;
39302
+ }
39303
+ }
39304
+ const orderedResults = chainEntries.map((e2) => resultsByChain.get(e2.chainKey)).filter((r3) => r3 !== void 0);
39305
+ const visible = opts.all ? orderedResults.filter((r3) => r3.total !== "0" || r3.tokens.length > 0) : orderedResults;
39306
+ const renderBlock = (r3, withSeparator) => {
39307
+ if (withSeparator) console.log("");
39308
+ console.log(import_chalk5.default.bold.underline(`${r3.chain} (${r3.symbol})`));
39309
+ console.log(` ${import_chalk5.default.dim("Address")} ${r3.address}`);
39310
+ console.log(` ${import_chalk5.default.dim("Available")} ${r3.available}`);
39311
+ if (r3.staked) console.log(` ${import_chalk5.default.dim("Staked")} ${r3.staked}`);
39312
+ const usdSuffix = r3.totalUsd !== null ? ` (\u2248 $${r3.totalUsd.toFixed(2)})` : "";
39313
+ console.log(` ${import_chalk5.default.dim("Total")} ${r3.total}${usdSuffix}`);
39314
+ if (r3.tokens.length > 0) {
39315
+ console.log(` ${import_chalk5.default.dim("Tokens:")}`);
39316
+ for (const t of r3.tokens) {
39317
+ console.log(` ${import_chalk5.default.bold(t.symbol.padEnd(10))} ${t.balance}`);
39477
39318
  }
39478
39319
  }
39320
+ };
39321
+ if (opts.all) {
39479
39322
  if (opts.json) {
39480
- output(results, true);
39323
+ output(visible, true);
39324
+ } else if (visible.length === 0) {
39325
+ info("No chains with non-zero native balance found.");
39481
39326
  } else {
39482
- if (results.length === 0) {
39483
- info("No chains with non-zero native balance found.");
39484
- } else {
39485
- for (let i2 = 0; i2 < results.length; i2++) {
39486
- const r3 = results[i2];
39487
- if (i2 > 0) console.log("");
39488
- console.log(import_chalk5.default.bold.underline(`${String(r3.chain)} (${String(r3.symbol)})`));
39489
- console.log(` ${import_chalk5.default.dim("Address")} ${r3.address}`);
39490
- console.log(` ${import_chalk5.default.dim("Available")} ${r3.available}`);
39491
- if (r3.staked) console.log(` ${import_chalk5.default.dim("Staked")} ${r3.staked}`);
39492
- if (r3.pending) console.log(` ${import_chalk5.default.dim("Pending")} ${r3.pending}`);
39493
- console.log(` ${import_chalk5.default.dim("Total")} ${r3.total}`);
39494
- const tokens = r3.tokens;
39495
- if (tokens.length > 0) {
39496
- console.log(` ${import_chalk5.default.dim("Tokens:")}`);
39497
- for (const t of tokens) {
39498
- console.log(` ${import_chalk5.default.bold(t.symbol.padEnd(10))} ${t.balance}`);
39499
- }
39500
- }
39501
- }
39502
- }
39327
+ visible.forEach((r3, i2) => renderBlock(r3, i2 > 0));
39503
39328
  }
39504
39329
  } else {
39505
- const chain2 = getChainById(opts.chain);
39506
- if (!chain2) {
39507
- throw new WalletError("CHAIN_UNSUPPORTED", `Unknown chain: ${opts.chain}`);
39508
- }
39509
- const address = await provider.getAddress(opts.chain);
39510
- const result = await fetchChainBalance(opts.chain, chain2, address, opts.tokens === false);
39330
+ const single = visible[0];
39511
39331
  if (opts.json) {
39512
- output(result, true);
39332
+ output(single, true);
39513
39333
  } else {
39514
- console.log(import_chalk5.default.bold.underline(`${result.chain} (${result.symbol})`));
39515
- console.log(` ${import_chalk5.default.dim("Address")} ${result.address}`);
39516
- console.log(` ${import_chalk5.default.dim("Available")} ${result.available}`);
39517
- if (result.staked) console.log(` ${import_chalk5.default.dim("Staked")} ${result.staked}`);
39518
- if (result.pending) console.log(` ${import_chalk5.default.dim("Pending")} ${result.pending}`);
39519
- console.log(` ${import_chalk5.default.dim("Total")} ${result.total}`);
39520
- if (result.tokens.length > 0) {
39521
- console.log(` ${import_chalk5.default.dim("Tokens:")}`);
39522
- for (const t of result.tokens) {
39523
- console.log(` ${import_chalk5.default.bold(t.symbol.padEnd(10))} ${t.balance}`);
39524
- }
39525
- }
39334
+ renderBlock(single, false);
39526
39335
  }
39527
39336
  }
39528
39337
  } catch (err) {
@@ -39670,23 +39479,18 @@ ${qr3}`;
39670
39479
  }
39671
39480
  safeExit();
39672
39481
  });
39673
- wallet.command("portfolio").description("Show portfolio balances and USD values across chains, including token holdings").option(
39674
- "--chains <list>",
39675
- "Comma-separated chain keys (default: all supported chains)",
39676
- Object.keys(CHAINS).join(",")
39677
- ).option(
39482
+ wallet.command("portfolio").description("Show portfolio balances and USD values across chains, including token holdings").option("--chains <list>", "Comma-separated chain keys (default: all supported chains)").option(
39678
39483
  "--password <password>",
39679
39484
  "Wallet password (falls back to keychain / TWAK_WALLET_PASSWORD)"
39680
39485
  ).option("--json", "JSON output").action(async (opts) => {
39681
39486
  try {
39682
- const creds = await ensureTrustSDK();
39487
+ await ensureTrustSDK();
39683
39488
  const password = await resolvePassword(opts);
39684
39489
  const provider = await AgentWalletProvider.fromStorage(password);
39685
- const { SecurityService: SecurityService2 } = await Promise.resolve().then(() => (init_dist5(), dist_exports3));
39686
39490
  const httpClient = await getClient();
39687
- const blockchainClient = provider.getBlockchainClient();
39688
39491
  const allAddresses = await provider.getAddresses();
39689
- const requestedChains = opts.chains.split(",").map((s2) => s2.trim()).filter(Boolean);
39492
+ const chainsArg = opts.chains ?? Object.keys(CHAINS).join(",");
39493
+ const requestedChains = chainsArg.split(",").map((s2) => s2.trim()).filter(Boolean);
39690
39494
  if (requestedChains.length === 0) {
39691
39495
  throw new WalletError("VALIDATION_ERROR", "--chains must include at least one chain key");
39692
39496
  }
@@ -39702,241 +39506,69 @@ ${qr3}`;
39702
39506
  `No supported chains found in --chains "${opts.chains}"`
39703
39507
  );
39704
39508
  }
39705
- const suppress = (fn2) => fn2();
39706
- const withTimeout = (promise, ms3) => {
39707
- let timerId;
39708
- const timeout = new Promise((_2, reject) => {
39709
- timerId = setTimeout(() => reject(new Error(`timed out after ${ms3 / 1e3}s`)), ms3);
39710
- });
39711
- return Promise.race([promise, timeout]).finally(() => {
39712
- if (timerId !== void 0) clearTimeout(timerId);
39713
- });
39714
- };
39715
- const pMap = async (items, fn2, concurrency) => {
39716
- const results = new Array(items.length);
39717
- let idx = 0;
39718
- const run = async () => {
39719
- while (idx < items.length) {
39720
- const i2 = idx++;
39721
- results[i2] = await fn2(items[i2]);
39722
- }
39723
- };
39724
- await Promise.all(
39725
- Array.from({ length: Math.min(concurrency, items.length) }, () => run())
39726
- );
39727
- return results;
39728
- };
39729
- const toHuman = (raw, decimals) => {
39730
- const big = BigInt(raw);
39731
- const div = 10n ** BigInt(decimals);
39732
- const intPart = big / div;
39733
- const fracPart = big % div;
39734
- const fracStr = fracPart.toString().padStart(decimals, "0").replace(/0+$/, "");
39735
- return fracStr ? `${intPart}.${fracStr}` : intPart.toString();
39736
- };
39737
- const holdingsAssets = chainEntries.map(
39738
- ({ chain: chain2, address }) => ({
39739
- coin: chain2.coinId,
39740
- address
39741
- })
39742
- );
39743
- const [nativeResults, holdings] = await Promise.all([
39744
- pMap(
39745
- chainEntries,
39746
- async ({
39747
- chainKey,
39748
- chain: chain2,
39749
- address
39750
- }) => {
39751
- try {
39752
- const balance = await withTimeout(
39753
- suppress(() => blockchainClient.getBalance(`c${chain2.coinId}`, address)),
39754
- 15e3
39755
- );
39756
- if (!balance) {
39757
- return {
39758
- chainKey,
39759
- chain: chain2,
39760
- address,
39761
- balance: null,
39762
- error: "empty response from SDK",
39763
- errorCode: "NETWORK_ERROR"
39764
- };
39765
- }
39766
- return {
39767
- chainKey,
39768
- chain: chain2,
39769
- address,
39770
- balance,
39771
- error: void 0,
39772
- errorCode: void 0
39773
- };
39774
- } catch (e2) {
39775
- const code2 = e2 != null ? e2.code : void 0;
39776
- const status = e2 != null ? e2.status : void 0;
39777
- let msg;
39778
- let errorCode = "NETWORK_ERROR";
39779
- if (status === 429 || code2 === 429) {
39780
- msg = "rate limited (429)";
39781
- errorCode = "RATE_LIMITED";
39782
- } else if (e2 instanceof Error && e2.message && e2.message !== "null") {
39783
- msg = e2.message;
39784
- } else if (code2) {
39785
- msg = `HTTP ${code2}`;
39786
- } else {
39787
- msg = "unsupported or unavailable";
39788
- }
39789
- return { chainKey, chain: chain2, address, balance: null, error: msg, errorCode };
39790
- }
39791
- },
39792
- 5
39793
- ),
39794
- (async () => {
39795
- try {
39796
- return await new SecurityService2(httpClient).getTokenHoldings(holdingsAssets);
39797
- } catch {
39798
- return [];
39799
- }
39800
- })()
39801
- ]);
39802
- const holdingsMap = /* @__PURE__ */ new Map();
39803
- for (const h4 of holdings) {
39804
- if (h4.tokens?.length) holdingsMap.set(h4.coin, h4.tokens);
39805
- }
39806
- if (holdingsMap.size === 0) {
39807
- try {
39808
- await registerWalletWithBackend(creds, provider);
39809
- const retryHoldings = await new SecurityService2(httpClient).getTokenHoldings(
39810
- holdingsAssets
39811
- );
39812
- for (const h4 of retryHoldings) {
39813
- if (h4.tokens?.length) holdingsMap.set(h4.coin, h4.tokens);
39814
- }
39815
- } catch {
39816
- }
39817
- }
39818
- for (const { chainKey, chain: chain2 } of chainEntries) {
39819
- const coinId = chain2.coinId;
39820
- if (holdingsMap.has(coinId)) continue;
39509
+ const chainAssets = chainEntries.map(({ chainKey, chain: chain2, address }) => ({
39510
+ chainKey,
39511
+ chain: chain2,
39512
+ token: void 0,
39513
+ assetId: `c${chain2.coinId}`,
39514
+ userAddress: address
39515
+ }));
39516
+ const tokenAssets = chainEntries.flatMap(({ chainKey, chain: chain2, address }) => {
39821
39517
  const knownTokens = COMMON_TOKENS[chainKey];
39822
- if (!knownTokens) continue;
39823
- const fallbackTokens = Object.entries(knownTokens).flatMap(([sym, addr]) => {
39518
+ if (!knownTokens) return [];
39519
+ return Object.keys(knownTokens).flatMap((sym) => {
39824
39520
  const resolved = resolveToken(sym, chainKey);
39825
39521
  if (!resolved) return [];
39826
- return [{ contract: addr, decimals: resolved.decimals, symbol: sym }];
39827
- });
39828
- holdingsMap.set(coinId, fallbackTokens);
39829
- }
39830
- const tokenJobs = [];
39831
- for (const { chainKey, chain: chain2, address } of chainEntries) {
39832
- const tokens = holdingsMap.get(chain2.coinId) ?? [];
39833
- for (const t of tokens) {
39834
- if (t.contract)
39835
- tokenJobs.push({
39522
+ return [
39523
+ {
39836
39524
  chainKey,
39837
- coinId: chain2.coinId,
39838
- address,
39839
- ...t
39840
- });
39841
- }
39842
- }
39843
- const tokenBalances = await pMap(
39844
- tokenJobs,
39845
- async (job) => {
39846
- const fallback = job.balance ?? "0";
39847
- try {
39848
- const assetId = `c${job.coinId}_t${job.contract}`;
39849
- const bal = await withTimeout(
39850
- suppress(() => blockchainClient.getBalance(assetId, job.address)),
39851
- 15e3
39852
- );
39853
- return { ...job, rawBalance: bal.total ?? bal.available ?? fallback };
39854
- } catch {
39855
- return { ...job, rawBalance: fallback };
39856
- }
39857
- },
39858
- 5
39859
- );
39860
- const nativeAssetIds = nativeResults.map(
39861
- ({ chain: chain2 }) => `c${chain2.coinId}`
39862
- );
39863
- const nonZeroTokens = tokenBalances.filter((t) => {
39864
- const dec = t.decimals ?? 18;
39865
- return toHuman(t.rawBalance, dec) !== "0";
39525
+ chain: chain2,
39526
+ token: { ...resolved, symbol: sym },
39527
+ assetId: `c${chain2.coinId}_t${resolved.address}`,
39528
+ userAddress: address
39529
+ }
39530
+ ];
39531
+ });
39866
39532
  });
39867
- const tokenAssetIds = nonZeroTokens.map(
39868
- (t) => `c${t.coinId}_t${t.contract}`
39533
+ const allAssets = [...chainAssets, ...tokenAssets];
39534
+ const balances = await fetchBalances(
39535
+ httpClient,
39536
+ allAssets.map(({ assetId, userAddress }) => ({ assetId, address: userAddress }))
39869
39537
  );
39870
- const priceMap = /* @__PURE__ */ new Map();
39871
- try {
39872
- const priceResp = await httpClient.market("POST", "/v2/market/tickers", {
39873
- currency: "USD",
39874
- assets: [.../* @__PURE__ */ new Set([...nativeAssetIds, ...tokenAssetIds])]
39875
- });
39876
- for (const ticker of priceResp.tickers || []) {
39877
- if (ticker.id && ticker.price != null) priceMap.set(ticker.id, ticker.price);
39878
- }
39879
- } catch {
39880
- }
39881
- const rows = [];
39882
- const failedChains = [];
39883
- for (const {
39884
- chainKey,
39885
- chain: chain2,
39886
- address,
39887
- balance,
39888
- error: fetchError,
39889
- errorCode: fetchErrorCode
39890
- } of nativeResults) {
39891
- const c3 = chain2;
39892
- if (fetchError) {
39893
- failedChains.push(`${chainKey} (${fetchError})`);
39894
- rows.push({
39895
- chain: chainKey,
39896
- type: "native",
39897
- symbol: c3.symbol,
39898
- address,
39899
- balance: "--",
39900
- usdValue: null,
39901
- error: fetchError,
39902
- errorCode: fetchErrorCode
39903
- });
39904
- continue;
39538
+ const assetsById = new Map(allAssets.map((a3) => [a3.assetId, a3]));
39539
+ const allRows = [];
39540
+ for (const balance of balances) {
39541
+ const assetInfo = assetsById.get(balance.assetId);
39542
+ if (!assetInfo) {
39543
+ throw new WalletError(
39544
+ "NETWORK_ERROR",
39545
+ "Could not fetch balances. Please try again later."
39546
+ );
39905
39547
  }
39906
- const price = priceMap.get(`c${c3.coinId}`) ?? null;
39907
- const rawTotal = balance?.total ?? balance?.available ?? "0";
39908
- const humanAmount = toHuman(rawTotal, c3.decimals);
39909
- const usdValue = price !== null ? parseFloat((parseFloat(humanAmount) * price).toFixed(2)) : null;
39910
- rows.push({
39548
+ const { token, chain: chain2, chainKey, userAddress } = assetInfo;
39549
+ const decimals = token ? token.decimals : chain2.decimals;
39550
+ const symbol = token ? token.symbol ?? token.address : chain2.symbol;
39551
+ const isToken = token !== void 0;
39552
+ const humanAmount = formatAmount(balance.amounts.total, decimals);
39553
+ const fiatRaw = balance.amounts.totalInFiat;
39554
+ const usdValue = fiatRaw && fiatRaw !== "" ? parseFloat(fiatRaw) : null;
39555
+ allRows.push({
39911
39556
  chain: chainKey,
39912
- type: "native",
39913
- symbol: c3.symbol,
39914
- address,
39915
- balance: humanAmount,
39916
- usdValue
39917
- });
39918
- }
39919
- for (const t of tokenBalances) {
39920
- const dec = t.decimals ?? 18;
39921
- const humanAmount = toHuman(t.rawBalance, dec);
39922
- if (humanAmount === "0") continue;
39923
- const tokenAssetId = `c${t.coinId}_t${t.contract}`;
39924
- const tokenPrice = priceMap.get(tokenAssetId) ?? null;
39925
- const usdValue = tokenPrice !== null ? parseFloat((parseFloat(humanAmount) * tokenPrice).toFixed(2)) : null;
39926
- rows.push({
39927
- chain: t.chainKey,
39928
- type: "token",
39929
- symbol: t.symbol ?? t.contract,
39930
- address: t.address,
39931
- contract: t.contract,
39557
+ type: isToken ? "token" : "native",
39558
+ symbol,
39559
+ address: userAddress,
39560
+ ...token ? { contract: token.address } : {},
39932
39561
  balance: humanAmount,
39933
39562
  usdValue
39934
39563
  });
39935
39564
  }
39565
+ const rows = allRows.filter((r3) => r3.type === "native" || r3.balance !== "0");
39566
+ rows.sort((a3, b3) => {
39567
+ const chainCmp = String(a3.chain).localeCompare(String(b3.chain));
39568
+ if (chainCmp !== 0) return chainCmp;
39569
+ return String(a3.type).localeCompare(String(b3.type));
39570
+ });
39936
39571
  if (opts.json) {
39937
- if (failedChains.length > 0) {
39938
- warn(`Balance unavailable for: ${failedChains.join(", ")}`);
39939
- }
39940
39572
  output(rows, true);
39941
39573
  } else {
39942
39574
  const totalUsd = rows.reduce(
@@ -39946,16 +39578,13 @@ ${qr3}`;
39946
39578
  console.log("Chain Type Symbol Balance USD");
39947
39579
  console.log("\u2500".repeat(72));
39948
39580
  for (const r3 of rows) {
39949
- const usd = r3.balance === "--" ? "" : r3.usdValue !== null ? `$${r3.usdValue.toFixed(2)}` : "";
39581
+ const usd = r3.usdValue !== null ? `$${r3.usdValue.toFixed(2)}` : "";
39950
39582
  console.log(
39951
39583
  `${String(r3.chain).padEnd(12)} ${String(r3.type).padEnd(7)} ${String(r3.symbol).padEnd(17)} ${String(r3.balance).padEnd(20)} ${usd}`
39952
39584
  );
39953
39585
  }
39954
39586
  console.log("\u2500".repeat(72));
39955
39587
  console.log(`Total USD: $${totalUsd.toFixed(2)}`);
39956
- if (failedChains.length > 0) {
39957
- warn(`Balance unavailable for: ${failedChains.join(", ")}`);
39958
- }
39959
39588
  }
39960
39589
  } catch (err) {
39961
39590
  handleCommandError(err, opts.json, "Portfolio failed");
@@ -40000,7 +39629,232 @@ ${qr3}`;
40000
39629
  // src/commands/transfer.ts
40001
39630
  init_dist();
40002
39631
  init_dist2();
40003
- init_dist5();
39632
+
39633
+ // ../security/dist/index.mjs
39634
+ init_dist();
39635
+ var SecurityService = class {
39636
+ constructor(client) {
39637
+ this.client = client;
39638
+ }
39639
+ async validate(params) {
39640
+ const query = new URLSearchParams();
39641
+ if (params.assetId) query.set("asset_id", params.assetId);
39642
+ query.set("address", params.address);
39643
+ if (params.type) query.set("type", params.type);
39644
+ if (params.version) query.set("version", params.version.toString());
39645
+ try {
39646
+ return await this.client.gateway(
39647
+ "GET",
39648
+ "/v1/validate",
39649
+ void 0,
39650
+ query.toString()
39651
+ );
39652
+ } catch (err) {
39653
+ const msg = err instanceof Error ? err.message : String(err);
39654
+ const details = err?.details;
39655
+ let detailsStr = "";
39656
+ if (details != null) {
39657
+ try {
39658
+ detailsStr = JSON.stringify(details);
39659
+ } catch {
39660
+ try {
39661
+ detailsStr = String(details);
39662
+ } catch {
39663
+ }
39664
+ }
39665
+ }
39666
+ if (msg.includes("GraphQL") || msg.includes("Failed to decode") || detailsStr.includes("GraphQL") || detailsStr.includes("Failed to decode")) {
39667
+ return { valid: false, message: "Invalid address format" };
39668
+ }
39669
+ throw err;
39670
+ }
39671
+ }
39672
+ async getAssetInfo(assetId) {
39673
+ const info2 = await this.client.gateway(
39674
+ "GET",
39675
+ `/v1/assets/${encodeURIComponent(assetId)}`
39676
+ );
39677
+ try {
39678
+ const priceResp = await this.client.market("POST", "/v2/market/tickers", { currency: "USD", assets: [assetId] });
39679
+ const ticker = priceResp.tickers?.[0];
39680
+ if (ticker) {
39681
+ if (ticker.price != null) info2.priceUsd = ticker.price;
39682
+ if (ticker.change_24h != null) info2.priceChange24h = ticker.change_24h;
39683
+ if (ticker.market_cap != null) info2.marketCapUsd = ticker.market_cap;
39684
+ if (ticker.volume_24h != null) info2.volume24hUsd = ticker.volume_24h;
39685
+ }
39686
+ } catch {
39687
+ }
39688
+ return info2;
39689
+ }
39690
+ async getTokenHoldings(assets, fromTime = 0) {
39691
+ const response = await this.client.gateway("POST", "/v1/assets", {
39692
+ from_time: fromTime,
39693
+ version: 2,
39694
+ assets
39695
+ });
39696
+ return Array.isArray(response) ? response : response.docs ?? response.data ?? [];
39697
+ }
39698
+ async checkCoinStatus(assetId) {
39699
+ const query = new URLSearchParams({
39700
+ version: "8.4",
39701
+ platform: "android",
39702
+ include_security_info: "true",
39703
+ include_solana_security_info: "true"
39704
+ });
39705
+ const raw = await this.client.gateway(
39706
+ "GET",
39707
+ `/v2/coinstatus/${encodeURIComponent(assetId)}`,
39708
+ void 0,
39709
+ query.toString()
39710
+ );
39711
+ return this.transformCoinStatus(assetId, raw);
39712
+ }
39713
+ async getAssetListings(params) {
39714
+ const limit = params.limit || 20;
39715
+ const query = new URLSearchParams({
39716
+ version: "27",
39717
+ currency: params.currency || "USD",
39718
+ limit: String(params.sort ? Math.max(limit, 50) : limit)
39719
+ });
39720
+ if (params.categoryId) query.set("category_id", params.categoryId);
39721
+ if (params.networks) query.set("networks", params.networks);
39722
+ const response = await this.client.gateway(
39723
+ "GET",
39724
+ "/v1/assets/listings",
39725
+ void 0,
39726
+ query.toString()
39727
+ );
39728
+ const docs = response.docs || response.results || [];
39729
+ const tokens = docs.map((doc) => {
39730
+ const asset = doc.asset || doc;
39731
+ const price = doc.price || {};
39732
+ const market = doc.market || {};
39733
+ const assetIdRaw = asset.asset_id || doc.asset_id || "";
39734
+ const coinId = assetIdRaw ? parseInt(assetIdRaw.replace(/^c/, "").split("_")[0], 10) : NaN;
39735
+ const chain2 = !isNaN(coinId) ? coinTypeToChainKey(coinId) || "" : "";
39736
+ return {
39737
+ assetId: assetIdRaw,
39738
+ name: asset.name || doc.name || "",
39739
+ symbol: asset.symbol || doc.symbol || "",
39740
+ chain: chain2,
39741
+ logoUrl: asset.icon_url || doc.icon_url,
39742
+ priceUsd: price.price ?? doc.price_usd,
39743
+ priceChange24h: price.percent_change_24h ?? doc.price_change_24h,
39744
+ marketCapUsd: market.market_cap ?? price.market_cap ?? doc.market_cap_usd,
39745
+ volume24hUsd: market.volume_24h ?? price.volume_24h ?? doc.volume_24h_usd
39746
+ };
39747
+ });
39748
+ if (params.sort) {
39749
+ const sortKeyMap = {
39750
+ percent_change_24h: "priceChange24h",
39751
+ market_cap: "marketCapUsd",
39752
+ volume_24h: "volume24hUsd"
39753
+ };
39754
+ const key = sortKeyMap[params.sort] || "priceChange24h";
39755
+ tokens.sort((a3, b3) => (b3[key] ?? 0) - (a3[key] ?? 0));
39756
+ }
39757
+ return tokens.slice(0, limit);
39758
+ }
39759
+ async getTrendingTokens(limit = 20, sort = "percent_change_24h") {
39760
+ const knownAssets = [
39761
+ "c60",
39762
+ "c501",
39763
+ "c0",
39764
+ "c20000714",
39765
+ "c966",
39766
+ "c10009000",
39767
+ "c8453",
39768
+ "c10042221",
39769
+ "c10000070",
39770
+ "c195",
39771
+ "c637",
39772
+ "c607",
39773
+ "c784",
39774
+ "c397",
39775
+ "c118",
39776
+ "c2",
39777
+ "c3",
39778
+ "c10000250",
39779
+ "c10000324",
39780
+ "c81457"
39781
+ ];
39782
+ const response = await this.client.market("POST", "/v2/market/tickers", {
39783
+ currency: "USD",
39784
+ assets: knownAssets
39785
+ });
39786
+ const tickers = response.tickers || [];
39787
+ const sortKey = sort === "market_cap" ? "market_cap" : sort === "volume_24h" ? "volume_24h" : "change_24h";
39788
+ return tickers.sort((a3, b3) => (b3[sortKey] ?? 0) - (a3[sortKey] ?? 0)).slice(0, limit).map((t) => {
39789
+ const coinId = t.id ? parseInt(t.id.replace(/^c/, ""), 10) : NaN;
39790
+ const chain2 = !isNaN(coinId) ? coinTypeToChainKey(coinId) || "" : "";
39791
+ return {
39792
+ assetId: t.id || "",
39793
+ name: t.slug || t.id || "",
39794
+ symbol: t.slug?.toUpperCase() || "",
39795
+ chain: chain2,
39796
+ logoUrl: void 0,
39797
+ priceUsd: t.price,
39798
+ priceChange24h: t.change_24h,
39799
+ marketCapUsd: t.market_cap,
39800
+ volume24hUsd: t.volume_24h
39801
+ };
39802
+ });
39803
+ }
39804
+ transformCoinStatus(assetId, raw) {
39805
+ if (!raw) return { assetId, isActive: false };
39806
+ const asset = raw.asset || raw;
39807
+ const security = raw.security_info || raw.securityInfo;
39808
+ const solanaSecurity = raw.solana_security_info || raw.solanaSecurityInfo;
39809
+ const warnings = [];
39810
+ let isHoneypot;
39811
+ if (security) {
39812
+ const allItems = [
39813
+ ...security.contract_security?.items || [],
39814
+ ...security.honeypot_risk?.items || []
39815
+ ];
39816
+ for (const item of allItems) {
39817
+ if (item.type === "warning" || item.type === "risk") {
39818
+ const message = item.summary ?? item.description ?? item.code;
39819
+ if (message) warnings.push(message);
39820
+ }
39821
+ if (item.code === "is_honeypot" && item.type === "risk") isHoneypot = true;
39822
+ }
39823
+ }
39824
+ const totalRisks = (security?.contract_security?.num_risks ?? 0) + (security?.honeypot_risk?.num_risks ?? 0);
39825
+ const totalWarnings = (security?.contract_security?.num_warnings ?? 0) + (security?.honeypot_risk?.num_warnings ?? 0);
39826
+ const computedRiskLevel = totalRisks > 0 ? "high" : totalWarnings > 0 ? "medium" : security ? "low" : void 0;
39827
+ let finalRiskLevel = security?.risk_level ?? security?.riskLevel ?? computedRiskLevel;
39828
+ const knownStablecoins = ["USDC", "USDT", "DAI", "BUSD", "TUSD", "USDP", "FRAX", "PYUSD"];
39829
+ const symbol = (asset.symbol ?? raw.symbol ?? "").toUpperCase();
39830
+ let riskContext;
39831
+ if (finalRiskLevel === "medium" && knownStablecoins.includes(symbol) && totalRisks === 0) {
39832
+ riskContext = "Warnings are due to proxy contract pattern, common in audited stablecoins";
39833
+ finalRiskLevel = "low";
39834
+ }
39835
+ return {
39836
+ assetId,
39837
+ name: asset.name ?? raw.name,
39838
+ symbol: asset.symbol ?? raw.symbol,
39839
+ isActive: raw.is_active ?? raw.isActive ?? true,
39840
+ supportsSwap: raw.isSwapEnabled ?? raw.supports_swap ?? raw.supportsSwap,
39841
+ supportsStaking: raw.isStakeEnabled ?? raw.supports_staking ?? raw.supportsStaking,
39842
+ securityInfo: security ? {
39843
+ isHoneypot: isHoneypot ?? security.is_honeypot ?? security.isHoneypot,
39844
+ hasAudit: security.has_audit ?? security.hasAudit ?? security.is_scanned,
39845
+ auditProvider: security.audit_provider ?? security.auditProvider ?? security.provider,
39846
+ riskLevel: finalRiskLevel,
39847
+ ...riskContext ? { riskContext } : {},
39848
+ warnings: warnings.length > 0 ? warnings : security.warnings
39849
+ } : void 0,
39850
+ solanaSecurityInfo: solanaSecurity ? {
39851
+ isMintable: solanaSecurity.is_mintable ?? solanaSecurity.isMintable,
39852
+ hasFreezeAuthority: solanaSecurity.has_freeze_authority ?? solanaSecurity.hasFreezeAuthority,
39853
+ riskLevel: solanaSecurity.risk_level ?? solanaSecurity.riskLevel
39854
+ } : void 0
39855
+ };
39856
+ }
39857
+ };
40004
39858
 
40005
39859
  // ../swap/dist/index.mjs
40006
39860
  init_dist();
@@ -40308,6 +40162,37 @@ var SwapExecutor = class {
40308
40162
 
40309
40163
  // src/commands/transfer.ts
40310
40164
  var DEFAULT_MAX_TRANSFER_USD = 1e4;
40165
+ function mapBroadcastError(err) {
40166
+ if (err instanceof CommandError) throw err;
40167
+ if (err instanceof TwakError) {
40168
+ if (err.code === "VALIDATION_ERROR") {
40169
+ throw new TransferError("VALIDATION_ERROR", err.message);
40170
+ }
40171
+ if (err.code === "NETWORK_ERROR") {
40172
+ throw new TransferError("NETWORK_ERROR", err.message);
40173
+ }
40174
+ if (err instanceof TwakApiError && err.statusCode === 429) {
40175
+ throw new TransferError("RATE_LIMITED", err.message);
40176
+ }
40177
+ if (err.code === "API_ERROR") {
40178
+ throw new TransferError("BROADCAST_FAILED", err.message);
40179
+ }
40180
+ }
40181
+ const msg = err instanceof Error ? err.message : String(err);
40182
+ if (/\binsufficient\b/i.test(msg)) {
40183
+ throw new TransferError("INSUFFICIENT_BALANCE", msg);
40184
+ }
40185
+ if (/\bnetwork\b|\becon(nrefused|timedout)\b|\bfetch\b/i.test(msg)) {
40186
+ throw new TransferError("NETWORK_ERROR", msg);
40187
+ }
40188
+ if (/\bbroadcast\b|\bsendtransaction\b|\brpc\b/i.test(msg)) {
40189
+ throw new TransferError("BROADCAST_FAILED", msg);
40190
+ }
40191
+ if (/\bsigning\b|\bsignature\b|\bsign\s+(error|failed)\b/i.test(msg)) {
40192
+ throw new TransferError("SIGN_FAILED", msg);
40193
+ }
40194
+ throw err;
40195
+ }
40311
40196
  async function resolvePassword2(opts) {
40312
40197
  if (opts.password) return opts.password;
40313
40198
  if (process.env.TWAK_WALLET_PASSWORD) return process.env.TWAK_WALLET_PASSWORD;
@@ -40424,35 +40309,7 @@ function registerTransferCommand(program3) {
40424
40309
  hdWallet
40425
40310
  });
40426
40311
  } catch (err) {
40427
- if (err instanceof CommandError) throw err;
40428
- if (err instanceof TwakError) {
40429
- if (err.code === "VALIDATION_ERROR") {
40430
- throw new TransferError("VALIDATION_ERROR", err.message);
40431
- }
40432
- if (err.code === "NETWORK_ERROR") {
40433
- throw new TransferError("NETWORK_ERROR", err.message);
40434
- }
40435
- if (err instanceof TwakApiError && err.statusCode === 429) {
40436
- throw new TransferError("RATE_LIMITED", err.message);
40437
- }
40438
- if (err.code === "API_ERROR") {
40439
- throw new TransferError("BROADCAST_FAILED", err.message);
40440
- }
40441
- }
40442
- const msg = err instanceof Error ? err.message : String(err);
40443
- if (/\binsufficient\b/i.test(msg)) {
40444
- throw new TransferError("INSUFFICIENT_BALANCE", msg);
40445
- }
40446
- if (/\bnetwork\b|\becon(nrefused|timedout)\b|\bfetch\b/i.test(msg)) {
40447
- throw new TransferError("NETWORK_ERROR", msg);
40448
- }
40449
- if (/\bbroadcast\b|\bsendtransaction\b|\brpc\b/i.test(msg)) {
40450
- throw new TransferError("BROADCAST_FAILED", msg);
40451
- }
40452
- if (/\bsigning\b|\bsignature\b|\bsign\s+(error|failed)\b/i.test(msg)) {
40453
- throw new TransferError("SIGN_FAILED", msg);
40454
- }
40455
- throw err;
40312
+ mapBroadcastError(err);
40456
40313
  }
40457
40314
  const explorerUrl = getExplorerTxUrl(result.chainKey, result.hash);
40458
40315
  success("Transfer sent!");
@@ -40971,7 +40828,6 @@ function registerErc20Command(program3) {
40971
40828
  // src/commands/alert.ts
40972
40829
  init_dist2();
40973
40830
  init_dist();
40974
- init_dist5();
40975
40831
  var assetCache = new AssetCache();
40976
40832
  async function resolveTokenLabel(tokenId, service) {
40977
40833
  const cached2 = await assetCache.get(tokenId);
@@ -41266,7 +41122,6 @@ var TransactionService = class {
41266
41122
  };
41267
41123
 
41268
41124
  // src/commands/query.ts
41269
- init_dist5();
41270
41125
  init_dist4();
41271
41126
  var import_chalk6 = __toESM(require_source());
41272
41127
  function handleError(err, json) {
@@ -41622,13 +41477,11 @@ function registerQueryCommand(program3) {
41622
41477
  if (err instanceof CommandError) throw err;
41623
41478
  }
41624
41479
  const assetId = opts.token ? `c${coinId}_t${opts.token}` : `c${coinId}`;
41625
- const client = new BlockchainClient();
41626
- const balance = await client.getBalance(assetId, opts.address);
41480
+ const httpClient = await getClient();
41627
41481
  let decimals = chain2.decimals;
41628
41482
  let symbol = chain2.symbol;
41629
41483
  if (opts.token) {
41630
- const apiClient = await getClient();
41631
- const swapClient = new SwapClient(apiClient);
41484
+ const swapClient = new SwapClient(httpClient);
41632
41485
  const chainKey = opts.chain || Object.entries(CHAINS).find(([, c3]) => c3.coinId === coinId)?.[0] || "";
41633
41486
  const tokens = await swapClient.searchTokens({
41634
41487
  query: opts.token,
@@ -41643,22 +41496,21 @@ function registerQueryCommand(program3) {
41643
41496
  symbol = opts.token;
41644
41497
  }
41645
41498
  }
41646
- const fmt = (raw) => {
41647
- const big = BigInt(raw);
41648
- if (decimals === 0) return big.toString();
41649
- const div = 10n ** BigInt(decimals);
41650
- return `${big / div}.${(big % div).toString().padStart(decimals, "0").slice(0, 6)}`;
41651
- };
41499
+ const balance = await fetchBalance(httpClient, assetId, opts.address);
41500
+ const amts = balance.amounts;
41501
+ const lockedHuman = formatAmount(amts.locked ?? "0", decimals);
41502
+ const fiatRaw = amts.totalInFiat;
41503
+ const totalUsd = fiatRaw && fiatRaw !== "" ? parseFloat(fiatRaw) : null;
41652
41504
  output(
41653
41505
  {
41654
41506
  address: opts.address,
41655
41507
  chain: chain2.id,
41656
41508
  symbol,
41657
41509
  ...opts.token ? { token: opts.token } : {},
41658
- available: fmt(balance.available),
41659
- ...balance.staked ? { staked: fmt(balance.staked) } : {},
41660
- ...balance.pending ? { pending: fmt(balance.pending) } : {},
41661
- total: fmt(balance.total),
41510
+ available: formatAmount(amts.available ?? "0", decimals),
41511
+ ...lockedHuman !== "0" ? { staked: lockedHuman } : {},
41512
+ total: formatAmount(amts.total ?? "0", decimals),
41513
+ totalUsd,
41662
41514
  raw: balance
41663
41515
  },
41664
41516
  opts.json
@@ -45895,12 +45747,11 @@ var NEVER = INVALID;
45895
45747
  // ../actions/dist/index.mjs
45896
45748
  init_dist();
45897
45749
  init_dist();
45898
- init_dist5();
45899
45750
  init_dist();
45900
45751
  init_dist();
45901
45752
  init_dist();
45902
45753
  init_dist3();
45903
- init_dist5();
45754
+ init_dist();
45904
45755
  init_dist();
45905
45756
  var ActionProvider = class {
45906
45757
  supportsChain(_chainId) {
@@ -46827,6 +46678,433 @@ var MarketActionProvider = class extends ActionProvider {
46827
46678
  };
46828
46679
  }
46829
46680
  };
46681
+ var ERC20_TRANSFER_SIG2 = "0xa9059cbb";
46682
+ var EVM_ADDRESS_RE = /^0x[a-fA-F0-9]{40}$/;
46683
+ var DEFAULT_MAX_OFFRAMP_USD = 1e4;
46684
+ function encodeErc20Transfer2(to3, amount) {
46685
+ const paddedTo = to3.slice(2).padStart(64, "0");
46686
+ const paddedAmount = amount.toString(16).padStart(64, "0");
46687
+ return `${ERC20_TRANSFER_SIG2}${paddedTo}${paddedAmount}`;
46688
+ }
46689
+ function buildFollowUp(cryptoAsset, address, direction, paymentMethod) {
46690
+ if (!cryptoAsset) return void 0;
46691
+ let parsed;
46692
+ try {
46693
+ parsed = parseTokenId(cryptoAsset);
46694
+ } catch {
46695
+ return void 0;
46696
+ }
46697
+ const isErc20 = Boolean(parsed.contractAddress);
46698
+ const action = isErc20 ? "get_token_holdings" : "get_balance";
46699
+ if (direction === "onramp") {
46700
+ return {
46701
+ action,
46702
+ arguments: { address, coin: parsed.coinId },
46703
+ why: "Verify the purchased crypto has arrived at the destination address. TWAK has no background polling or completion webhook \u2014 the only way to confirm delivery is to poll the balance yourself.",
46704
+ expectedChange: "increase",
46705
+ pollEveryMs: 6e4,
46706
+ typicalCompletion: "Card purchases usually settle within 5\u201330 minutes; bank transfers can take 1\u20133 business days. Stop polling once the balance has increased or after a reasonable ceiling (e.g., 1 hour for card, 4 days for bank)."
46707
+ };
46708
+ }
46709
+ return {
46710
+ action,
46711
+ arguments: { address, coin: parsed.coinId },
46712
+ why: "Poll this only AFTER calling confirm_offramp_transfer. The browser-hosted provider page cannot sign transactions (it has no access to the wallet keys). Required flow: (1) user opens the checkout URL and completes KYC + order creation in the browser, (2) the provider displays a deposit address (and memo/tag on some chains) + exact amount, (3) the agent calls confirm_offramp_transfer with those values to broadcast the payout natively, (4) then this balance-poll confirms the crypto has left the source address.",
46713
+ expectedChange: "decrease",
46714
+ pollEveryMs: 3e4,
46715
+ typicalCompletion: "Balance decrease is visible within 1\u20132 minutes of confirm_offramp_transfer returning a tx hash. The subsequent fiat payout is processed separately by the provider (typically 1\u20133 business days for bank_transfer, faster for card).",
46716
+ notes: paymentMethod ? `Fiat payout method: ${paymentMethod}` : void 0
46717
+ };
46718
+ }
46719
+ function mapQuote(q3) {
46720
+ return {
46721
+ quoteId: q3.id,
46722
+ cryptoAmount: q3.digital_money?.amount,
46723
+ cryptoCurrency: q3.digital_money?.currency,
46724
+ fiatAmount: q3.fiat_money?.amount,
46725
+ fiatCurrency: q3.fiat_money?.currency,
46726
+ provider: q3.provider?.name,
46727
+ paymentMethod: q3.payment_method?.payment_method,
46728
+ isRecommended: q3.is_recommended
46729
+ };
46730
+ }
46731
+ function summarizeOnrampQuotes(quotes) {
46732
+ if (quotes.length === 0) return "No quotes available for the requested fiat\u2192crypto purchase.";
46733
+ const best = quotes[0];
46734
+ const worstVsBest = quotes[quotes.length - 1].vsBestPct ?? 0;
46735
+ const savingsHint = worstVsBest > 0 ? ` (saves ${worstVsBest.toFixed(2)}% vs cheapest)` : "";
46736
+ return `${quotes.length} quote${quotes.length === 1 ? "" : "s"} available \u2014 best: ${best.cryptoAmount} ${best.cryptoCurrency} for ${best.fiatAmount} ${best.fiatCurrency} via ${best.provider}${savingsHint}.`;
46737
+ }
46738
+ function summarizeOfframpQuotes(quotes) {
46739
+ if (quotes.length === 0) return "No quotes available for the requested crypto\u2192fiat sale.";
46740
+ const best = quotes[0];
46741
+ const worstVsBest = quotes[quotes.length - 1].vsBestPct ?? 0;
46742
+ const savingsHint = worstVsBest > 0 ? ` (saves ${worstVsBest.toFixed(2)}% vs cheapest)` : "";
46743
+ return `${quotes.length} quote${quotes.length === 1 ? "" : "s"} available \u2014 best: ${best.fiatAmount} ${best.fiatCurrency} via ${best.provider}${savingsHint}.`;
46744
+ }
46745
+ var OnrampActionProvider = class extends ActionProvider {
46746
+ // An optional wallet provider lets the action auto-derive the caller's own
46747
+ // address from the asset's chain, matching the safety contract that
46748
+ // transfer/swap/erc20 actions already follow. Explicit `walletAddress`
46749
+ // overrides the derivation for agents operating on behalf of other users.
46750
+ //
46751
+ // `priceLookup` + `maxTransferUsd` enable the same USD safety cap used by
46752
+ // TransferActionProvider for the off-ramp payout path (confirm_offramp_transfer).
46753
+ // Off-ramp sends crypto to a third-party deposit address, so the cap is
46754
+ // strictly more important here than for peer-to-peer transfers.
46755
+ constructor(walletProvider, priceLookup, maxTransferUsd = DEFAULT_MAX_OFFRAMP_USD) {
46756
+ super();
46757
+ this.walletProvider = walletProvider;
46758
+ this.priceLookup = priceLookup;
46759
+ this.maxTransferUsd = maxTransferUsd;
46760
+ }
46761
+ name = "onramp";
46762
+ 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.";
46763
+ getActions() {
46764
+ return [
46765
+ this.getOnrampQuote(),
46766
+ this.getOnrampUrl(),
46767
+ this.getOfframpQuote(),
46768
+ this.getOfframpUrl(),
46769
+ this.confirmOfframpTransfer()
46770
+ ];
46771
+ }
46772
+ async resolveAddress(providedAddress, cryptoAsset) {
46773
+ if (providedAddress) return providedAddress;
46774
+ if (!this.walletProvider) {
46775
+ throw new Error(
46776
+ "walletAddress not provided and no wallet provider is configured. Pass walletAddress explicitly or configure an agent wallet."
46777
+ );
46778
+ }
46779
+ if (!cryptoAsset) {
46780
+ throw new Error(
46781
+ "walletAddress not provided and cryptoAsset is missing \u2014 cannot derive chain. Pass walletAddress or cryptoAsset."
46782
+ );
46783
+ }
46784
+ const chainKey = tokenIdToChain(cryptoAsset);
46785
+ if (!chainKey) {
46786
+ throw new Error(
46787
+ `Cannot determine chain for asset ${cryptoAsset}. Pass walletAddress explicitly.`
46788
+ );
46789
+ }
46790
+ return this.walletProvider.getAddress(chainKey);
46791
+ }
46792
+ getOnrampQuote() {
46793
+ return {
46794
+ name: "get_onramp_quote",
46795
+ 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.",
46796
+ schema: external_exports.object({
46797
+ fiatAmount: external_exports.coerce.number().positive().describe("Amount of fiat currency to spend (e.g., 100 for $100)."),
46798
+ fiatCurrency: external_exports.string().length(3).default("USD").describe("Fiat currency as a 3-letter ISO 4217 code (e.g., USD, EUR, GBP)."),
46799
+ cryptoAsset: external_exports.string().describe(
46800
+ "Target crypto asset ID. Format: c<coinId> for native coins (e.g., c60 for ETH, c0 for BTC, c501 for SOL) or c<coinId>_t<contract> for tokens (e.g., c60_t0xA0b8... for USDC on Ethereum)."
46801
+ ),
46802
+ walletAddress: external_exports.string().optional().describe(
46803
+ "Destination wallet address where the purchased crypto will be sent. OMIT to auto-derive from the configured wallet using cryptoAsset's chain \u2014 this is the safe default and prevents sending funds to a stranger by mistake. Only supply this when operating on behalf of a different user."
46804
+ )
46805
+ }),
46806
+ invoke: async (ctx, input) => {
46807
+ const address = await this.resolveAddress(input.walletAddress, input.cryptoAsset);
46808
+ const query = new URLSearchParams({
46809
+ amount: input.fiatAmount.toString(),
46810
+ currency: input.fiatCurrency,
46811
+ address
46812
+ });
46813
+ const result = await ctx.httpClient.gateway(
46814
+ "GET",
46815
+ `/v1/buycrypto/quote/${encodeURIComponent(input.cryptoAsset)}`,
46816
+ void 0,
46817
+ query.toString()
46818
+ );
46819
+ const mapped = (result?.quotes ?? []).map(mapQuote);
46820
+ const quotes = rankQuotesByBest(mapped, (q3) => q3.cryptoAmount);
46821
+ return {
46822
+ success: true,
46823
+ summary: summarizeOnrampQuotes(quotes),
46824
+ address,
46825
+ cryptoAsset: input.cryptoAsset,
46826
+ quotes,
46827
+ nextAction: quotes.length > 0 ? "Call get_onramp_url with the chosen quoteId to receive a browser redirect URL for checkout. quotes[0] is the best deal (lowest vsBestPct)." : "No quotes available \u2014 try a different fiatAmount or cryptoAsset."
46828
+ };
46829
+ }
46830
+ };
46831
+ }
46832
+ getOnrampUrl() {
46833
+ return {
46834
+ name: "get_onramp_url",
46835
+ 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.",
46836
+ schema: external_exports.object({
46837
+ quoteId: external_exports.string().describe("Quote ID returned by get_onramp_quote."),
46838
+ walletAddress: external_exports.string().optional().describe(
46839
+ "Destination wallet address. OMIT to auto-derive from the configured wallet \u2014 requires cryptoAsset so the chain can be determined. Only supply this when operating on behalf of a different user."
46840
+ ),
46841
+ cryptoAsset: external_exports.string().optional().describe(
46842
+ "Crypto asset ID that was used for the quote (e.g., c60 for ETH). Required when walletAddress is omitted so the chain can be derived for address lookup."
46843
+ )
46844
+ }),
46845
+ invoke: async (ctx, input) => {
46846
+ const address = await this.resolveAddress(input.walletAddress, input.cryptoAsset);
46847
+ const query = new URLSearchParams({ address });
46848
+ let result;
46849
+ try {
46850
+ result = await ctx.httpClient.gateway(
46851
+ "GET",
46852
+ `/v1/buycrypto/request/${encodeURIComponent(input.quoteId)}`,
46853
+ void 0,
46854
+ query.toString()
46855
+ );
46856
+ } catch (err) {
46857
+ if (err instanceof TwakApiError && err.statusCode === 404) {
46858
+ return {
46859
+ success: false,
46860
+ address,
46861
+ summary: `No checkout URL returned for quoteId ${input.quoteId} \u2014 the quote has likely expired (buy quotes are short-lived). Request a fresh quote with get_onramp_quote and retry.`,
46862
+ nextAction: "Call get_onramp_quote again to obtain a fresh quoteId, then retry get_onramp_url."
46863
+ };
46864
+ }
46865
+ throw err;
46866
+ }
46867
+ const url = result.url ?? result.redirect_url;
46868
+ const followUp = url ? buildFollowUp(input.cryptoAsset, address, "onramp") : void 0;
46869
+ return {
46870
+ success: Boolean(url),
46871
+ address,
46872
+ url,
46873
+ summary: url ? `Open the URL in a browser to complete your purchase. Crypto will be delivered to ${address}.` : "No checkout URL returned \u2014 the quoteId may have expired. Request a new quote with get_onramp_quote.",
46874
+ followUp
46875
+ };
46876
+ }
46877
+ };
46878
+ }
46879
+ getOfframpQuote() {
46880
+ return {
46881
+ name: "get_offramp_quote",
46882
+ 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.",
46883
+ schema: external_exports.object({
46884
+ cryptoAmount: external_exports.coerce.number().positive().describe("Amount of crypto to sell (e.g., 0.1 for 0.1 ETH)."),
46885
+ cryptoAsset: external_exports.string().describe(
46886
+ "Crypto asset ID to sell. Format: c<coinId> for native coins (e.g., c60 for ETH, c0 for BTC) or c<coinId>_t<contract> for tokens."
46887
+ ),
46888
+ fiatCurrency: external_exports.string().length(3).default("USD").describe("Target fiat currency as a 3-letter ISO 4217 code (e.g., USD, EUR, GBP)."),
46889
+ paymentMethod: external_exports.enum(["ANY", "bank_transfer", "card"]).optional().default("ANY").describe(
46890
+ 'Payout method. Default "ANY" is a sentinel that matches any provider \u2014 recommended unless you specifically need "card" or "bank_transfer". Sending an unrecognised value (or `bank_transfer` in regions without bank-payout coverage) silently returns an empty quotes array.'
46891
+ ),
46892
+ walletAddress: external_exports.string().optional().describe(
46893
+ "Source wallet address holding the crypto to be sold. OMIT to auto-derive from the configured wallet using cryptoAsset's chain \u2014 this is the safe default. Only supply this when operating on behalf of a different user."
46894
+ )
46895
+ }),
46896
+ invoke: async (ctx, input) => {
46897
+ const address = await this.resolveAddress(input.walletAddress, input.cryptoAsset);
46898
+ const query = new URLSearchParams({
46899
+ amount: input.cryptoAmount.toString(),
46900
+ currency: input.fiatCurrency,
46901
+ // iOS sends `asset_id` as both path segment AND query param. Mirroring that.
46902
+ asset_id: input.cryptoAsset,
46903
+ address,
46904
+ // The HTTP gateway uses `payment_method` for both buy and sell, mirroring the
46905
+ // iOS production client. The sell proto declares `payout_method` but the
46906
+ // gateway doesn't honour it; trust runtime over proto.
46907
+ payment_method: input.paymentMethod ?? "ANY",
46908
+ // iOS always sends this even when the user holds no TWT; omitting it appears
46909
+ // to filter out fee-waiver-aware providers in some regions.
46910
+ twt_amount: "0"
46911
+ });
46912
+ const result = await ctx.httpClient.gateway(
46913
+ "GET",
46914
+ `/v1/sellcrypto/quote/${encodeURIComponent(input.cryptoAsset)}`,
46915
+ void 0,
46916
+ query.toString()
46917
+ );
46918
+ const mapped = (result?.quotes ?? []).filter((q3) => ALLOWED_OFFRAMP_PROVIDERS.has(q3.provider?.name ?? "")).map(mapQuote);
46919
+ const quotes = rankQuotesByBest(mapped, (q3) => q3.fiatAmount);
46920
+ return {
46921
+ success: true,
46922
+ summary: summarizeOfframpQuotes(quotes),
46923
+ address,
46924
+ cryptoAsset: input.cryptoAsset,
46925
+ quotes,
46926
+ nextAction: quotes.length > 0 ? "Call get_offramp_url with the chosen quoteId to receive a browser redirect URL for the sale. quotes[0] is the best deal (lowest vsBestPct)." : "No quotes available \u2014 try a different cryptoAmount, cryptoAsset, or paymentMethod."
46927
+ };
46928
+ }
46929
+ };
46930
+ }
46931
+ getOfframpUrl() {
46932
+ return {
46933
+ name: "get_offramp_url",
46934
+ 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.",
46935
+ schema: external_exports.object({
46936
+ quoteId: external_exports.string().describe("Quote ID returned by get_offramp_quote."),
46937
+ walletAddress: external_exports.string().optional().describe(
46938
+ "Source wallet address. OMIT to auto-derive from the configured wallet \u2014 requires cryptoAsset so the chain can be determined. Only supply this when operating on behalf of a different user."
46939
+ ),
46940
+ cryptoAsset: external_exports.string().optional().describe(
46941
+ "Crypto asset ID that was used for the quote (e.g., c60 for ETH). Required when walletAddress is omitted so the chain can be derived for address lookup."
46942
+ )
46943
+ }),
46944
+ invoke: async (ctx, input) => {
46945
+ const address = await this.resolveAddress(input.walletAddress, input.cryptoAsset);
46946
+ const query = new URLSearchParams({ address });
46947
+ let result;
46948
+ try {
46949
+ result = await ctx.httpClient.gateway(
46950
+ "GET",
46951
+ `/v1/sellcrypto/request/${encodeURIComponent(input.quoteId)}`,
46952
+ void 0,
46953
+ query.toString()
46954
+ );
46955
+ } catch (err) {
46956
+ if (err instanceof TwakApiError && err.statusCode === 404) {
46957
+ return {
46958
+ success: false,
46959
+ address,
46960
+ summary: `No checkout URL returned for quoteId ${input.quoteId} \u2014 the quote has likely expired (sell quotes are short-lived). Request a fresh quote with get_offramp_quote and retry.`,
46961
+ nextAction: "Call get_offramp_quote again to obtain a fresh quoteId, then retry get_offramp_url."
46962
+ };
46963
+ }
46964
+ throw err;
46965
+ }
46966
+ const url = result.url ?? result.redirect_url;
46967
+ const followUp = url ? buildFollowUp(input.cryptoAsset, address, "offramp") : void 0;
46968
+ return {
46969
+ success: Boolean(url),
46970
+ address,
46971
+ url,
46972
+ summary: url ? `Open the URL in a browser to complete KYC and create the sell order from ${address}. The provider will display a deposit address (and memo/tag on chains that need one) plus the exact amount to send. Then call confirm_offramp_transfer with those values to broadcast the payout \u2014 the browser cannot sign it.` : "No checkout URL returned \u2014 the quoteId may have expired. Request a new quote with get_offramp_quote.",
46973
+ nextAction: url ? "After the user reads the provider's deposit address + amount, call confirm_offramp_transfer with those values to broadcast the payout transaction." : void 0,
46974
+ followUp
46975
+ };
46976
+ }
46977
+ };
46978
+ }
46979
+ confirmOfframpTransfer() {
46980
+ return {
46981
+ name: "confirm_offramp_transfer",
46982
+ 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 for BTC / Solana / Cosmos off-ramps use the `twak onramp sell-confirm` CLI command instead. Returns a tx hash on success; fiat payout from the provider then arrives separately (no on-chain signal for that step).",
46983
+ schema: external_exports.object({
46984
+ depositAddress: external_exports.string().describe(
46985
+ "The deposit address displayed by the provider after KYC. This is a third-party address \u2014 verify it matches what the provider shows before calling."
46986
+ ),
46987
+ cryptoAmount: external_exports.string().describe(
46988
+ 'Exact amount to send in human-readable format (e.g., "0.1" for 0.1 ETH). Must match the amount the provider displays; under-sending will invalidate the sell order.'
46989
+ ),
46990
+ cryptoAsset: external_exports.string().describe(
46991
+ "Crypto asset being sold. Format: c<coinId> for native coins (e.g., c60 for ETH) or c<coinId>_t<contract> for ERC-20 tokens."
46992
+ ),
46993
+ decimals: external_exports.coerce.number().optional().describe(
46994
+ "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."
46995
+ ),
46996
+ memo: external_exports.string().optional().describe(
46997
+ "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."
46998
+ ),
46999
+ quoteId: external_exports.string().optional().describe(
47000
+ "Quote ID from get_offramp_quote. Optional \u2014 included only for traceability in the response; tx details come from depositAddress / cryptoAmount."
47001
+ )
47002
+ }),
47003
+ invoke: async (_ctx, input) => {
47004
+ const signer = this.walletProvider;
47005
+ if (!signer) {
47006
+ return { success: false, error: "No wallet provider configured" };
47007
+ }
47008
+ let parsed;
47009
+ try {
47010
+ parsed = parseTokenId(input.cryptoAsset);
47011
+ } catch {
47012
+ throw new TransferError("VALIDATION_ERROR", `Invalid cryptoAsset: ${input.cryptoAsset}`);
47013
+ }
47014
+ const chainKey = coinTypeToChainKey(parsed.coinId);
47015
+ if (!chainKey) {
47016
+ throw new TransferError(
47017
+ "CHAIN_UNSUPPORTED",
47018
+ `Unsupported coin type in cryptoAsset: ${input.cryptoAsset}`
47019
+ );
47020
+ }
47021
+ const chainDef = getChainById(chainKey);
47022
+ if (!chainDef || chainDef.namespace !== "eip155") {
47023
+ throw new TransferError(
47024
+ "CHAIN_UNSUPPORTED",
47025
+ `confirm_offramp_transfer is EVM-only. For ${chainKey} off-ramps use the CLI: twak onramp sell-confirm --asset ${input.cryptoAsset} --to <deposit> --amount <n>${input.memo ? " --memo <tag>" : ""}.`
47026
+ );
47027
+ }
47028
+ const caip2 = `eip155:${chainDef.chainId}`;
47029
+ if (!EVM_ADDRESS_RE.test(input.depositAddress)) {
47030
+ throw new TransferError(
47031
+ "VALIDATION_ERROR",
47032
+ `depositAddress is not a valid EVM address: ${input.depositAddress}. Double-check what the provider displayed.`
47033
+ );
47034
+ }
47035
+ if (parsed.contractAddress && !EVM_ADDRESS_RE.test(parsed.contractAddress)) {
47036
+ throw new TransferError(
47037
+ "VALIDATION_ERROR",
47038
+ `Malformed token contract in cryptoAsset: ${parsed.contractAddress}`
47039
+ );
47040
+ }
47041
+ if (parsed.contractAddress && input.decimals === void 0) {
47042
+ throw new TransferError(
47043
+ "VALIDATION_ERROR",
47044
+ `decimals is required for ERC-20 asset ${input.cryptoAsset}. 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.`
47045
+ );
47046
+ }
47047
+ const decimals = parsed.contractAddress ? input.decimals : 18;
47048
+ try {
47049
+ assertStrictPositiveAmount(input.cryptoAmount, decimals);
47050
+ } catch (err) {
47051
+ throw new TransferError(
47052
+ "VALIDATION_ERROR",
47053
+ err instanceof Error ? err.message : String(err)
47054
+ );
47055
+ }
47056
+ if (this.priceLookup) {
47057
+ try {
47058
+ const tokenInfo = resolveToken(input.cryptoAsset, chainKey);
47059
+ const tokenAddress = tokenInfo?.address ?? parsed.contractAddress ?? "";
47060
+ const price = await this.priceLookup.getTokenPrice(chainKey, tokenAddress);
47061
+ if (price && price > 0) {
47062
+ const usdValue = parseFloat(input.cryptoAmount) * price;
47063
+ if (usdValue > this.maxTransferUsd) {
47064
+ return {
47065
+ success: false,
47066
+ error: `Off-ramp payout value ~$${usdValue.toFixed(2)} USD exceeds safety limit of $${this.maxTransferUsd}. Use the CLI with --max-usd ${Math.ceil(usdValue)} or --skip-safety-check to proceed.`
47067
+ };
47068
+ }
47069
+ }
47070
+ } catch {
47071
+ }
47072
+ }
47073
+ const atomic = parseAmount(input.cryptoAmount, decimals);
47074
+ let txHash;
47075
+ if (parsed.contractAddress) {
47076
+ const calldata = encodeErc20Transfer2(input.depositAddress, BigInt(atomic));
47077
+ txHash = await signer.sendTransaction({
47078
+ chainId: caip2,
47079
+ to: parsed.contractAddress,
47080
+ data: calldata,
47081
+ value: "0x0"
47082
+ });
47083
+ } else {
47084
+ txHash = await signer.sendTransaction({
47085
+ chainId: caip2,
47086
+ to: input.depositAddress,
47087
+ data: "0x",
47088
+ value: "0x" + BigInt(atomic).toString(16)
47089
+ });
47090
+ }
47091
+ const explorerUrl = getExplorerTxUrl(chainKey, txHash) || void 0;
47092
+ return {
47093
+ success: true,
47094
+ txHash,
47095
+ chain: chainKey,
47096
+ to: input.depositAddress,
47097
+ amount: input.cryptoAmount,
47098
+ asset: input.cryptoAsset,
47099
+ memoIgnored: input.memo ? `A memo was supplied (${input.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,
47100
+ quoteId: input.quoteId,
47101
+ explorer: explorerUrl,
47102
+ summary: `Off-ramp payout of ${input.cryptoAmount} ${input.cryptoAsset} broadcast to ${input.depositAddress} on ${chainKey}. Fiat payout from the provider arrives separately.`
47103
+ };
47104
+ }
47105
+ };
47106
+ }
47107
+ };
46830
47108
  var AutomateActionProvider = class extends ActionProvider {
46831
47109
  constructor(automationStore, swapClient, walletProvider, blockchainClient) {
46832
47110
  super();
@@ -55882,6 +56160,7 @@ function registerServeCommand(program3) {
55882
56160
  registry2.register(new AgentWalletActionProvider(blockchainClient));
55883
56161
  registry2.register(new TokenActionProvider(blockchainClient, agentWallet));
55884
56162
  registry2.register(new MarketActionProvider());
56163
+ registry2.register(new OnrampActionProvider(walletProvider, swapClient));
55885
56164
  registry2.register(
55886
56165
  new AutomateActionProvider(automationStore, swapClient, walletProvider, blockchainClient)
55887
56166
  );
@@ -55916,11 +56195,435 @@ function registerServeCommand(program3) {
55916
56195
  });
55917
56196
  }
55918
56197
 
56198
+ // src/commands/onramp.ts
56199
+ var import_chalk7 = __toESM(require_source());
56200
+ init_dist();
56201
+ var DEFAULT_MAX_OFFRAMP_USD2 = 1e4;
56202
+ function fmtVsBest(pct) {
56203
+ return pct === 0 ? "0.00%" : `-${pct.toFixed(2)}%`;
56204
+ }
56205
+ function printBuyQuoteTable(rows, fiatAmount, fiatCurrency, asset) {
56206
+ const cols = rows.map((r3) => ({
56207
+ provider: r3.provider ?? "",
56208
+ pay: `${r3.fiatAmount ?? ""} ${r3.fiatCurrency ?? ""}`.trim(),
56209
+ receive: `${r3.cryptoAmount ?? ""} ${r3.cryptoCurrency ?? ""}`.trim(),
56210
+ vsBest: fmtVsBest(r3.vsBestPct),
56211
+ quoteId: r3.quoteId ?? ""
56212
+ }));
56213
+ const w4 = {
56214
+ provider: Math.max(8, ...cols.map((c3) => c3.provider.length)),
56215
+ pay: Math.max(3, ...cols.map((c3) => c3.pay.length)),
56216
+ receive: Math.max(7, ...cols.map((c3) => c3.receive.length)),
56217
+ vsBest: Math.max(7, ...cols.map((c3) => c3.vsBest.length))
56218
+ };
56219
+ const cryptoCurrency = rows[0]?.cryptoCurrency ?? "";
56220
+ console.log(`${rows.length} quotes for ${fiatAmount} ${fiatCurrency} \u2192 ${cryptoCurrency}
56221
+ `);
56222
+ console.log(
56223
+ import_chalk7.default.bold(
56224
+ `${"PROVIDER".padEnd(w4.provider)} ${"PAY".padEnd(w4.pay)} ${"RECEIVE".padEnd(w4.receive)} ${"VS BEST".padEnd(w4.vsBest)} QUOTE ID`
56225
+ )
56226
+ );
56227
+ for (const c3 of cols) {
56228
+ console.log(
56229
+ `${c3.provider.padEnd(w4.provider)} ${c3.pay.padEnd(w4.pay)} ${c3.receive.padEnd(w4.receive)} ${c3.vsBest.padEnd(w4.vsBest)} ${c3.quoteId}`
56230
+ );
56231
+ }
56232
+ const best = rows[0];
56233
+ console.log(
56234
+ `
56235
+ \u2192 best: ${import_chalk7.default.bold(best.provider ?? "")} \u2014 run: ${import_chalk7.default.bold(`twak onramp buy --quote-id ${best.quoteId} --asset ${asset}`)}`
56236
+ );
56237
+ }
56238
+ function printSellQuoteTable(rows, cryptoAmount, cryptoAsset) {
56239
+ const cols = rows.map((r3) => ({
56240
+ provider: r3.provider ?? "",
56241
+ pay: `${r3.cryptoAmount ?? ""} ${r3.cryptoCurrency ?? ""}`.trim(),
56242
+ receive: `${r3.fiatAmount ?? ""} ${r3.fiatCurrency ?? ""}`.trim(),
56243
+ vsBest: fmtVsBest(r3.vsBestPct),
56244
+ quoteId: r3.quoteId ?? ""
56245
+ }));
56246
+ const w4 = {
56247
+ provider: Math.max(8, ...cols.map((c3) => c3.provider.length)),
56248
+ pay: Math.max(3, ...cols.map((c3) => c3.pay.length)),
56249
+ receive: Math.max(7, ...cols.map((c3) => c3.receive.length)),
56250
+ vsBest: Math.max(7, ...cols.map((c3) => c3.vsBest.length))
56251
+ };
56252
+ const fiatCurrency = rows[0]?.fiatCurrency ?? "";
56253
+ console.log(`${rows.length} quotes for ${cryptoAmount} ${cryptoAsset} \u2192 ${fiatCurrency}
56254
+ `);
56255
+ console.log(
56256
+ import_chalk7.default.bold(
56257
+ `${"PROVIDER".padEnd(w4.provider)} ${"PAY".padEnd(w4.pay)} ${"RECEIVE".padEnd(w4.receive)} ${"VS BEST".padEnd(w4.vsBest)} QUOTE ID`
56258
+ )
56259
+ );
56260
+ for (const c3 of cols) {
56261
+ console.log(
56262
+ `${c3.provider.padEnd(w4.provider)} ${c3.pay.padEnd(w4.pay)} ${c3.receive.padEnd(w4.receive)} ${c3.vsBest.padEnd(w4.vsBest)} ${c3.quoteId}`
56263
+ );
56264
+ }
56265
+ const best = rows[0];
56266
+ console.log(
56267
+ `
56268
+ \u2192 best: ${import_chalk7.default.bold(best.provider ?? "")} \u2014 run: ${import_chalk7.default.bold(`twak onramp sell --quote-id ${best.quoteId} --asset ${cryptoAsset}`)}`
56269
+ );
56270
+ }
56271
+ async function resolveWalletAddress(opts, context) {
56272
+ if (opts.wallet) {
56273
+ warn(
56274
+ `Using manually specified ${context} address ${opts.wallet}. This is not derived from your stored wallet \u2014 double-check it before proceeding.`
56275
+ );
56276
+ return opts.wallet;
56277
+ }
56278
+ if (!opts.asset) {
56279
+ throw new Error(
56280
+ "Provide --wallet, or pass --asset so the address can be derived from your stored wallet."
56281
+ );
56282
+ }
56283
+ const chainKey = tokenIdToChain(opts.asset);
56284
+ if (!chainKey) {
56285
+ throw new Error(
56286
+ `Cannot determine chain for asset ${opts.asset}. Pass --wallet explicitly or use a supported asset ID.`
56287
+ );
56288
+ }
56289
+ await ensureTrustSDK();
56290
+ const password = await resolvePassword({ password: opts.password });
56291
+ const provider = await AgentWalletProvider.fromStorage(password);
56292
+ const address = await provider.getAddress(chainKey);
56293
+ info(`Using ${context} address ${address} on ${chainKey} (derived from your stored wallet).`);
56294
+ return address;
56295
+ }
56296
+ function registerOnrampCommand(program3) {
56297
+ const onramp = program3.command("onramp").description("Buy or sell crypto with fiat currency");
56298
+ onramp.command("quote").description("Get quotes for buying crypto with fiat currency").requiredOption("--amount <n>", "Fiat amount (e.g., 100)").requiredOption("--asset <id>", "Crypto asset ID (e.g., c60 for ETH)").option(
56299
+ "--wallet <address>",
56300
+ "Destination wallet address (defaults to your stored wallet address for the asset's chain)"
56301
+ ).option("--currency <code>", "Fiat currency code", "USD").option(
56302
+ "--password <password>",
56303
+ "Wallet password (only needed when --wallet is omitted; falls back to keychain / TWAK_WALLET_PASSWORD)"
56304
+ ).option("--json", "JSON output").action(async (opts) => {
56305
+ try {
56306
+ const address = await resolveWalletAddress(opts, "destination");
56307
+ const client = await getClient();
56308
+ const query = new URLSearchParams({
56309
+ amount: opts.amount,
56310
+ currency: opts.currency,
56311
+ address
56312
+ });
56313
+ const result = await client.gateway(
56314
+ "GET",
56315
+ `/v1/buycrypto/quote/${encodeURIComponent(opts.asset)}`,
56316
+ void 0,
56317
+ query.toString()
56318
+ );
56319
+ const mapped = (result?.quotes || []).map((q3) => ({
56320
+ quoteId: q3.id,
56321
+ cryptoAmount: q3.digital_money?.amount,
56322
+ cryptoCurrency: q3.digital_money?.currency,
56323
+ fiatAmount: q3.fiat_money?.amount,
56324
+ fiatCurrency: q3.fiat_money?.currency,
56325
+ provider: q3.provider?.name,
56326
+ paymentMethod: q3.payment_method?.payment_method
56327
+ }));
56328
+ const quotes = rankQuotesByBest(mapped, (q3) => q3.cryptoAmount);
56329
+ if (quotes.length === 0) {
56330
+ if (!opts.json)
56331
+ info(
56332
+ `No quotes available for ${opts.amount} ${opts.currency} \u2192 ${opts.asset}. Try a different amount, currency, or asset.`
56333
+ );
56334
+ output([], opts.json);
56335
+ } else if (opts.json) {
56336
+ output(quotes, true);
56337
+ } else {
56338
+ printBuyQuoteTable(quotes, opts.amount, opts.currency, opts.asset);
56339
+ }
56340
+ } catch (err) {
56341
+ handleCommandError(err, opts.json, "Quote failed");
56342
+ }
56343
+ safeExit();
56344
+ });
56345
+ onramp.command("buy").description("Get a URL to complete a fiat-to-crypto purchase (pass quoteId from onramp quote)").requiredOption("--quote-id <id>", "Quote ID from onramp quote").option(
56346
+ "--wallet <address>",
56347
+ "Destination wallet address (defaults to your stored wallet address for the asset's chain)"
56348
+ ).option(
56349
+ "--asset <id>",
56350
+ "Crypto asset ID (e.g., c60) \u2014 used to derive your address when --wallet is omitted"
56351
+ ).option(
56352
+ "--password <password>",
56353
+ "Wallet password (only needed when --wallet is omitted; falls back to keychain / TWAK_WALLET_PASSWORD)"
56354
+ ).option("--json", "JSON output").action(async (opts) => {
56355
+ try {
56356
+ const address = await resolveWalletAddress(opts, "destination");
56357
+ const client = await getClient();
56358
+ const query = new URLSearchParams({ address });
56359
+ const result = await client.gateway(
56360
+ "GET",
56361
+ `/v1/buycrypto/request/${encodeURIComponent(opts.quoteId)}`,
56362
+ void 0,
56363
+ query.toString()
56364
+ );
56365
+ const url = result.url ?? result.redirect_url;
56366
+ if (!opts.json && url) {
56367
+ info(`Open this URL to complete your purchase:
56368
+ ${url}`);
56369
+ const chainKey = opts.asset ? tokenIdToChain(opts.asset) : void 0;
56370
+ if (chainKey) {
56371
+ info(
56372
+ `After payment completes (typically 5\u201330 min for card, 1\u20133 business days for bank transfer), check your balance with:
56373
+ twak wallet balance --chain ${chainKey}`
56374
+ );
56375
+ } else {
56376
+ info(
56377
+ "After payment completes, check your balance with: twak wallet balance --chain <chain>"
56378
+ );
56379
+ }
56380
+ }
56381
+ output({ url }, opts.json);
56382
+ } catch (err) {
56383
+ handleCommandError(err, opts.json, "Buy failed");
56384
+ }
56385
+ safeExit();
56386
+ });
56387
+ onramp.command("sell-quote").description("Get quotes for selling crypto to receive fiat").requiredOption("--amount <n>", "Crypto amount to sell (e.g., 0.1)").requiredOption("--asset <id>", "Crypto asset ID to sell (e.g., c60 for ETH)").option(
56388
+ "--wallet <address>",
56389
+ "Source wallet address (defaults to your stored wallet address for the asset's chain)"
56390
+ ).option("--currency <code>", "Fiat currency code", "USD").option(
56391
+ "--method <method>",
56392
+ "Payout method: ANY (default sentinel \u2014 matches any provider), card, bank_transfer",
56393
+ "ANY"
56394
+ ).option(
56395
+ "--password <password>",
56396
+ "Wallet password (only needed when --wallet is omitted; falls back to keychain / TWAK_WALLET_PASSWORD)"
56397
+ ).option("--json", "JSON output").action(async (opts) => {
56398
+ try {
56399
+ const address = await resolveWalletAddress(opts, "source");
56400
+ const client = await getClient();
56401
+ const query = new URLSearchParams({
56402
+ amount: opts.amount,
56403
+ currency: opts.currency,
56404
+ // iOS sends `asset_id` as both path segment AND query param. Mirroring that —
56405
+ // the gateway appears to require the query form even when the path has it.
56406
+ asset_id: opts.asset,
56407
+ address,
56408
+ // The HTTP gateway uses `payment_method` for both buy and sell, even though
56409
+ // the sell proto declares `payout_method` — runtime evidence (iOS production
56410
+ // client) is the source of truth here.
56411
+ payment_method: opts.method,
56412
+ // iOS always sends this even when the user holds no TWT; omitting it appears
56413
+ // to filter out fee-waiver-aware providers in some regions.
56414
+ twt_amount: "0"
56415
+ });
56416
+ const result = await client.gateway(
56417
+ "GET",
56418
+ `/v1/sellcrypto/quote/${encodeURIComponent(opts.asset)}`,
56419
+ void 0,
56420
+ query.toString()
56421
+ );
56422
+ const mapped = (result?.quotes || []).filter((q3) => ALLOWED_OFFRAMP_PROVIDERS.has(q3.provider?.name ?? "")).map((q3) => ({
56423
+ quoteId: q3.id,
56424
+ cryptoAmount: q3.digital_money?.amount,
56425
+ cryptoCurrency: q3.digital_money?.currency,
56426
+ fiatAmount: q3.fiat_money?.amount,
56427
+ fiatCurrency: q3.fiat_money?.currency,
56428
+ provider: q3.provider?.name,
56429
+ isRecommended: q3.is_recommended
56430
+ }));
56431
+ const quotes = rankQuotesByBest(mapped, (q3) => q3.fiatAmount);
56432
+ if (quotes.length === 0) {
56433
+ if (!opts.json)
56434
+ info(
56435
+ `No quotes available for ${opts.amount} ${opts.asset} \u2192 ${opts.currency}. Try a different amount, asset, or currency.`
56436
+ );
56437
+ output([], opts.json);
56438
+ } else if (opts.json) {
56439
+ output(quotes, true);
56440
+ } else {
56441
+ printSellQuoteTable(quotes, opts.amount, opts.asset);
56442
+ }
56443
+ } catch (err) {
56444
+ handleCommandError(err, opts.json, "Sell quote failed");
56445
+ }
56446
+ safeExit();
56447
+ });
56448
+ onramp.command("sell").description(
56449
+ "Get a URL to complete a crypto-to-fiat sale (pass quoteId from onramp sell-quote)"
56450
+ ).requiredOption("--quote-id <id>", "Quote ID from onramp sell-quote").option(
56451
+ "--wallet <address>",
56452
+ "Source wallet address (defaults to your stored wallet address for the asset's chain)"
56453
+ ).option(
56454
+ "--asset <id>",
56455
+ "Crypto asset ID (e.g., c60) \u2014 used to derive your address when --wallet is omitted"
56456
+ ).option(
56457
+ "--password <password>",
56458
+ "Wallet password (only needed when --wallet is omitted; falls back to keychain / TWAK_WALLET_PASSWORD)"
56459
+ ).option("--json", "JSON output").action(async (opts) => {
56460
+ try {
56461
+ const address = await resolveWalletAddress(opts, "source");
56462
+ const client = await getClient();
56463
+ const query = new URLSearchParams({ address });
56464
+ const result = await client.gateway(
56465
+ "GET",
56466
+ `/v1/sellcrypto/request/${encodeURIComponent(opts.quoteId)}`,
56467
+ void 0,
56468
+ query.toString()
56469
+ );
56470
+ const url = result.url ?? result.redirect_url;
56471
+ if (!opts.json && url) {
56472
+ info(`Open this URL and complete KYC + the sell order in your browser:
56473
+ ${url}`);
56474
+ info(
56475
+ "The provider will show a deposit address (and a memo/tag for chains that need one) plus the exact amount to send. The browser has no access to your keys \u2014 it cannot sign the transfer itself. Copy those values back here and finalise the payout with:"
56476
+ );
56477
+ const assetArg = opts.asset ? ` --asset ${opts.asset}` : " --asset <assetId>";
56478
+ info(
56479
+ ` twak onramp sell-confirm${assetArg} --to <deposit-address> --amount <exact-amount> [--memo <tag>]`
56480
+ );
56481
+ info(
56482
+ "Once sell-confirm broadcasts, fiat arrives via the provider separately (typically 1\u20133 business days for bank transfer, faster for card)."
56483
+ );
56484
+ }
56485
+ output({ url }, opts.json);
56486
+ } catch (err) {
56487
+ handleCommandError(err, opts.json, "Sell failed");
56488
+ }
56489
+ safeExit();
56490
+ });
56491
+ onramp.command("sell-confirm").description(
56492
+ "Finalise an off-ramp by broadcasting the payout transaction to the provider's deposit address. Run this after completing KYC in the browser and copying the deposit address + exact amount shown by the provider."
56493
+ ).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(
56494
+ "--amount <n>",
56495
+ "Exact amount to send, in human-readable format (must match the amount the provider displays)"
56496
+ ).option(
56497
+ "--memo <tag>",
56498
+ "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."
56499
+ ).option(
56500
+ "--quote-id <id>",
56501
+ "Quote ID from onramp sell-quote. Optional \u2014 included only for traceability in the output; the authoritative tx details come from --to / --amount."
56502
+ ).option(
56503
+ "--max-usd <n>",
56504
+ `Maximum allowed payout value in USD (default: ${DEFAULT_MAX_OFFRAMP_USD2})`
56505
+ ).option("--skip-safety-check", "Skip the USD-value safety cap").option(
56506
+ "--password <password>",
56507
+ "Wallet password (falls back to keychain / TWAK_WALLET_PASSWORD)"
56508
+ ).option("--json", "JSON output").action(async (opts) => {
56509
+ try {
56510
+ await ensureTrustSDK();
56511
+ const password = await resolvePassword({ password: opts.password });
56512
+ const provider = await AgentWalletProvider.fromStorage(password);
56513
+ let coinId;
56514
+ try {
56515
+ ({ coinId } = parseTokenId(opts.asset));
56516
+ } catch {
56517
+ throw new TransferError("VALIDATION_ERROR", `Invalid asset ID: ${opts.asset}`);
56518
+ }
56519
+ const chainKey = coinTypeToChainKey(coinId);
56520
+ if (!chainKey) {
56521
+ throw new TransferError(
56522
+ "CHAIN_UNSUPPORTED",
56523
+ `Unsupported coin type in asset ID: ${opts.asset}`
56524
+ );
56525
+ }
56526
+ const parsedAmount = parseFloat(opts.amount);
56527
+ if (!Number.isFinite(parsedAmount) || parsedAmount <= 0) {
56528
+ throw new TransferError("VALIDATION_ERROR", "--amount must be a positive number");
56529
+ }
56530
+ const httpClient = await getClient();
56531
+ let decimals;
56532
+ try {
56533
+ const assetInfo = await new SecurityService(httpClient).getAssetInfo(opts.asset);
56534
+ decimals = assetInfo.decimals;
56535
+ } catch {
56536
+ }
56537
+ try {
56538
+ assertStrictPositiveAmount(opts.amount, decimals);
56539
+ } catch (err) {
56540
+ throw new TransferError(
56541
+ "VALIDATION_ERROR",
56542
+ err instanceof Error ? err.message : String(err)
56543
+ );
56544
+ }
56545
+ if (!opts.skipSafetyCheck) {
56546
+ const maxUsd = parseFloat(opts.maxUsd ?? String(DEFAULT_MAX_OFFRAMP_USD2));
56547
+ if (!Number.isFinite(maxUsd) || maxUsd <= 0) {
56548
+ throw new TransferError("VALIDATION_ERROR", "--max-usd must be a positive number");
56549
+ }
56550
+ try {
56551
+ const tokenInfo = resolveToken(opts.asset, chainKey);
56552
+ if (tokenInfo) {
56553
+ const swapClient = new SwapClient(httpClient);
56554
+ const price = await swapClient.getTokenPrice(chainKey, tokenInfo.address);
56555
+ if (price && price > 0) {
56556
+ const usdValue = parsedAmount * price;
56557
+ if (usdValue > maxUsd) {
56558
+ throw new TransferError(
56559
+ "USD_LIMIT_EXCEEDED",
56560
+ `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.`
56561
+ );
56562
+ }
56563
+ if (usdValue > 1e3) {
56564
+ warn(`High-value off-ramp payout: ~$${usdValue.toFixed(2)} USD`);
56565
+ }
56566
+ }
56567
+ }
56568
+ } catch (err) {
56569
+ if (err instanceof CommandError) throw err;
56570
+ warn(
56571
+ "Could not verify payout USD value (price lookup failed). Use --skip-safety-check to suppress."
56572
+ );
56573
+ }
56574
+ }
56575
+ const address = await provider.getAddress(chainKey);
56576
+ warn(
56577
+ `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.`
56578
+ );
56579
+ const client = provider.getBlockchainClient();
56580
+ const hdWallet = provider.getSigner().getHDWallet();
56581
+ let result;
56582
+ try {
56583
+ result = await client.transfer({
56584
+ assetId: opts.asset,
56585
+ to: opts.to,
56586
+ amount: opts.amount,
56587
+ decimals,
56588
+ memo: opts.memo,
56589
+ hdWallet
56590
+ });
56591
+ } catch (err) {
56592
+ mapBroadcastError(err);
56593
+ }
56594
+ const explorerUrl = getExplorerTxUrl(result.chainKey, result.hash);
56595
+ success("Off-ramp payout broadcast!");
56596
+ if (!opts.json) {
56597
+ info(
56598
+ "Fiat will be credited by the provider separately (typically 1\u20133 business days for bank transfer, faster for card). There is no on-chain signal for that step \u2014 watch for the provider's email/SMS confirmation."
56599
+ );
56600
+ }
56601
+ output(
56602
+ {
56603
+ hash: result.hash,
56604
+ chain: result.chainKey,
56605
+ from: address,
56606
+ to: opts.to,
56607
+ amount: opts.amount,
56608
+ asset: opts.asset,
56609
+ memo: opts.memo,
56610
+ quoteId: opts.quoteId,
56611
+ explorer: explorerUrl
56612
+ },
56613
+ opts.json
56614
+ );
56615
+ } catch (err) {
56616
+ handleCommandError(err, opts.json, "Sell confirm failed");
56617
+ }
56618
+ safeExit();
56619
+ });
56620
+ }
56621
+
55919
56622
  // src/commands/automate.ts
55920
56623
  init_dist2();
55921
56624
  init_dist();
55922
56625
  function parseInterval(value) {
55923
- const match = value.match(/^(\d+(?:\.\d+)?)\s*(s|m|h)?$/i);
56626
+ const match = value.match(/^(\d+(?:\.\d+)?)\s*(s|m|h|d)?$/i);
55924
56627
  if (!match) return null;
55925
56628
  const num = parseFloat(match[1]);
55926
56629
  if (isNaN(num)) return null;
@@ -55928,9 +56631,11 @@ function parseInterval(value) {
55928
56631
  if (unit === "s") return num * 1e3;
55929
56632
  if (unit === "m") return num * 6e4;
55930
56633
  if (unit === "h") return num * 36e5;
56634
+ if (unit === "d") return num * 864e5;
55931
56635
  return num * 1e3;
55932
56636
  }
55933
56637
  function formatInterval(ms3) {
56638
+ if (ms3 >= 864e5 && ms3 % 864e5 === 0) return `${ms3 / 864e5}d`;
55934
56639
  if (ms3 >= 36e5) return `${ms3 / 36e5}h`;
55935
56640
  if (ms3 >= 6e4) return `${ms3 / 6e4}m`;
55936
56641
  return `${ms3 / 1e3}s`;
@@ -55942,8 +56647,10 @@ function formatRelativeTime(diffMs) {
55942
56647
  value = `${Math.round(absDiff / 1e3)}s`;
55943
56648
  } else if (absDiff < 36e5) {
55944
56649
  value = `${Math.round(absDiff / 6e4)}m`;
55945
- } else {
56650
+ } else if (absDiff < 864e5) {
55946
56651
  value = `${Math.round(absDiff / 36e5)}h`;
56652
+ } else {
56653
+ value = `${Math.round(absDiff / 864e5)}d`;
55947
56654
  }
55948
56655
  if (diffMs <= 0) return "overdue";
55949
56656
  return `in ${value}`;
@@ -55957,7 +56664,7 @@ function formatTimeAgo(isoDate) {
55957
56664
  }
55958
56665
  function registerAutomateCommand(program3) {
55959
56666
  const automate = program3.command("automate").description("Manage DCA and limit order automations");
55960
- automate.command("add").description("Create a DCA or limit order automation").requiredOption("--from <token>", "Source token (e.g., USDC)").requiredOption("--to <token>", "Destination token (e.g., ETH)").option("--chain <chain>", "Chain", "ethereum").requiredOption("--amount <n>", "Amount of source token per run").option("--interval <duration>", "DCA interval \u2014 e.g. 30s, 5m, 1h (creates DCA automation)").option("--price <usd>", "Target price in USD (creates limit order)").option("--condition <cond>", "Price condition: above or below", "below").option("--max-runs <n>", "Stop after N executions").option("--expires <date>", "Expiry date (ISO 8601, e.g. 2026-04-01)").option("--json", "JSON output").action(async (opts) => {
56667
+ automate.command("add").description("Create a DCA or limit order automation").requiredOption("--from <token>", "Source token (e.g., USDC)").requiredOption("--to <token>", "Destination token (e.g., ETH)").option("--chain <chain>", "Chain", "ethereum").requiredOption("--amount <n>", "Amount of source token per run").option("--interval <duration>", "DCA interval \u2014 e.g. 30s, 5m, 1h, 7d (creates DCA automation)").option("--price <usd>", "Target price in USD (creates limit order)").option("--condition <cond>", "Price condition: above or below", "below").option("--max-runs <n>", "Stop after N executions").option("--expires <date>", "Expiry date (ISO 8601, e.g. 2026-04-01)").option("--json", "JSON output").action(async (opts) => {
55961
56668
  try {
55962
56669
  if (opts.interval && opts.price) {
55963
56670
  throw new AutomationError(
@@ -56326,7 +57033,7 @@ var crypto6 = __toESM(require("crypto"));
56326
57033
  // package.json
56327
57034
  var package_default = {
56328
57035
  name: "@trustwallet/cli",
56329
- version: "0.10.0",
57036
+ version: "0.11.0",
56330
57037
  description: "TWAK CLI \u2014 command-line interface for Trust Wallet Agent Kit",
56331
57038
  main: "dist/index.js",
56332
57039
  types: "dist/index.d.ts",
@@ -56495,13 +57202,14 @@ function ts3() {
56495
57202
  return (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 19);
56496
57203
  }
56497
57204
  function parseInterval2(value) {
56498
- const match = value.match(/^(\d+(?:\.\d+)?)\s*(s|m|h)?$/i);
57205
+ const match = value.match(/^(\d+(?:\.\d+)?)\s*(s|m|h|d)?$/i);
56499
57206
  if (!match) return null;
56500
57207
  const num = parseFloat(match[1]);
56501
57208
  if (isNaN(num)) return null;
56502
57209
  const unit = (match[2] || "s").toLowerCase();
56503
57210
  if (unit === "m") return num * 60;
56504
57211
  if (unit === "h") return num * 3600;
57212
+ if (unit === "d") return num * 86400;
56505
57213
  return num;
56506
57214
  }
56507
57215
  function registerWatchCommand(program3) {
@@ -56509,7 +57217,7 @@ function registerWatchCommand(program3) {
56509
57217
  "Background watcher \u2014 polls prices to trigger alerts and execute DCA/limit automations"
56510
57218
  ).option(
56511
57219
  "--interval <duration>",
56512
- "Poll interval \u2014 e.g. 30s, 5m, 1h, or plain number (seconds)",
57220
+ "Poll interval \u2014 e.g. 30s, 5m, 1h, 1d, or plain number (seconds)",
56513
57221
  "60"
56514
57222
  ).option("--password <password>", "Wallet password for executing swaps").option("--auto-lock <minutes>", "Auto-lock wallet after N minutes of inactivity").option("--dry-run", "Check conditions without executing swaps").option("--json", "JSON output per tick").action(async (opts) => {
56515
57223
  let client;
@@ -56836,6 +57544,7 @@ registerErc20Command(program2);
56836
57544
  registerAlertCommand(program2);
56837
57545
  registerQueryCommand(program2);
56838
57546
  registerServeCommand(program2);
57547
+ registerOnrampCommand(program2);
56839
57548
  registerAutomateCommand(program2);
56840
57549
  registerX402Command(program2);
56841
57550
  registerTelemetryCommand(program2);