@veil-cash/sdk 0.4.0 → 0.5.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.
@@ -6,26 +6,24 @@ import { Command } from 'commander';
6
6
  import { Keypair } from '../../keypair.js';
7
7
  import { transfer, mergeUtxos } from '../../transfer.js';
8
8
  import { handleCLIError, CLIError, ErrorCode } from '../errors.js';
9
+ import { clearProgress, createProgressReporter, printFields, printHeader, printJson, printLine, txUrl } from '../output.js';
9
10
  import type { RelayPool } from '../../types.js';
10
11
 
11
12
  const SUPPORTED_ASSETS = ['ETH', 'USDC'];
12
13
 
13
- // Progress helper - writes to stderr so JSON output stays clean
14
- function progress(msg: string, quiet?: boolean) {
15
- if (!quiet) {
16
- process.stderr.write(`\r\x1b[K${msg}`);
17
- }
18
- }
19
-
20
14
  export function createTransferCommand(): Command {
21
15
  const transferCmd = new Command('transfer')
22
16
  .description('Transfer privately within the pool to another registered address')
23
17
  .argument('<asset>', 'Asset to transfer (ETH or USDC)')
24
18
  .argument('<amount>', 'Amount to transfer (e.g., 0.1)')
25
19
  .argument('<recipient>', 'Recipient address (must be registered)')
26
- .option('--veil-key <key>', 'Veil private key (or set VEIL_KEY env)')
27
- .option('--rpc-url <url>', 'RPC URL (or set RPC_URL env)')
28
- .option('--quiet', 'Suppress progress output')
20
+ .option('--json', 'Output as JSON')
21
+ .addHelpText('after', `
22
+ Examples:
23
+ veil transfer ETH 0.02 0xRecipientAddress
24
+ veil transfer USDC 25 0xRecipientAddress
25
+ veil transfer ETH 0.02 0xRecipientAddress --json
26
+ `)
29
27
  .action(async (asset: string, amount: string, recipient: string, options) => {
30
28
  try {
31
29
  const assetUpper = asset.toUpperCase();
@@ -41,24 +39,16 @@ export function createTransferCommand(): Command {
41
39
  }
42
40
 
43
41
  // Get keypair
44
- const veilKey = options.veilKey || process.env.VEIL_KEY;
42
+ const veilKey = process.env.VEIL_KEY;
45
43
  if (!veilKey) {
46
- throw new CLIError(ErrorCode.VEIL_KEY_MISSING, 'VEIL_KEY required. Use --veil-key or set VEIL_KEY env');
44
+ throw new CLIError(ErrorCode.VEIL_KEY_MISSING, 'VEIL_KEY required. Set VEIL_KEY env');
47
45
  }
48
46
 
49
47
  const senderKeypair = new Keypair(veilKey);
50
- const rpcUrl = options.rpcUrl || process.env.RPC_URL;
48
+ const rpcUrl = process.env.RPC_URL;
51
49
  const pool = assetUpper.toLowerCase() as RelayPool;
52
-
53
- // Progress callback
54
- const onProgress = options.quiet
55
- ? undefined
56
- : (stage: string, detail?: string) => {
57
- const msg = detail ? `${stage}: ${detail}` : stage;
58
- progress(msg, options.quiet);
59
- };
60
-
61
- progress(`Starting ${assetUpper} transfer...`, options.quiet);
50
+ const onProgress = createProgressReporter();
51
+ onProgress(`Starting ${assetUpper} transfer...`);
62
52
 
63
53
  // Execute transfer
64
54
  const result = await transfer({
@@ -70,11 +60,9 @@ export function createTransferCommand(): Command {
70
60
  onProgress,
71
61
  });
72
62
 
73
- // Clear progress line
74
- progress('', options.quiet);
63
+ clearProgress();
75
64
 
76
- // Output result
77
- console.log(JSON.stringify({
65
+ const output = {
78
66
  success: result.success,
79
67
  transactionHash: result.transactionHash,
80
68
  blockNumber: result.blockNumber,
@@ -82,10 +70,24 @@ export function createTransferCommand(): Command {
82
70
  amount: result.amount,
83
71
  recipient: result.recipient,
84
72
  type: 'transfer',
85
- }, null, 2));
86
- process.exit(0);
73
+ };
74
+
75
+ if (options.json) {
76
+ printJson(output);
77
+ return;
78
+ }
79
+
80
+ printHeader('Transfer Submitted');
81
+ printFields([
82
+ { label: 'Asset', value: assetUpper },
83
+ { label: 'Amount', value: result.amount },
84
+ { label: 'Recipient', value: result.recipient },
85
+ { label: 'Transaction', value: txUrl(result.transactionHash) },
86
+ { label: 'Block', value: result.blockNumber },
87
+ ]);
88
+ printLine();
87
89
  } catch (error) {
88
- progress('', options.quiet);
90
+ clearProgress();
89
91
  handleCLIError(error);
90
92
  }
91
93
  });
@@ -98,9 +100,13 @@ export function createMergeCommand(): Command {
98
100
  .description('Merge UTXOs by self-transfer (consolidate small UTXOs)')
99
101
  .argument('<asset>', 'Asset to merge (ETH or USDC)')
100
102
  .argument('<amount>', 'Amount to merge (e.g., 0.5)')
101
- .option('--veil-key <key>', 'Veil private key (or set VEIL_KEY env)')
102
- .option('--rpc-url <url>', 'RPC URL (or set RPC_URL env)')
103
- .option('--quiet', 'Suppress progress output')
103
+ .option('--json', 'Output as JSON')
104
+ .addHelpText('after', `
105
+ Examples:
106
+ veil merge ETH 0.1
107
+ veil merge USDC 100
108
+ veil merge ETH 0.1 --json
109
+ `)
104
110
  .action(async (asset: string, amount: string, options) => {
105
111
  try {
106
112
  const assetUpper = asset.toUpperCase();
@@ -111,24 +117,16 @@ export function createMergeCommand(): Command {
111
117
  }
112
118
 
113
119
  // Get keypair
114
- const veilKey = options.veilKey || process.env.VEIL_KEY;
120
+ const veilKey = process.env.VEIL_KEY;
115
121
  if (!veilKey) {
116
- throw new CLIError(ErrorCode.VEIL_KEY_MISSING, 'VEIL_KEY required. Use --veil-key or set VEIL_KEY env');
122
+ throw new CLIError(ErrorCode.VEIL_KEY_MISSING, 'VEIL_KEY required. Set VEIL_KEY env');
117
123
  }
118
124
 
119
125
  const keypair = new Keypair(veilKey);
120
- const rpcUrl = options.rpcUrl || process.env.RPC_URL;
126
+ const rpcUrl = process.env.RPC_URL;
121
127
  const pool = assetUpper.toLowerCase() as RelayPool;
122
-
123
- // Progress callback
124
- const onProgress = options.quiet
125
- ? undefined
126
- : (stage: string, detail?: string) => {
127
- const msg = detail ? `${stage}: ${detail}` : stage;
128
- progress(msg, options.quiet);
129
- };
130
-
131
- progress(`Starting ${assetUpper} merge (self-transfer)...`, options.quiet);
128
+ const onProgress = createProgressReporter();
129
+ onProgress(`Starting ${assetUpper} merge (self-transfer)...`);
132
130
 
133
131
  // Execute merge
134
132
  const result = await mergeUtxos({
@@ -139,21 +137,32 @@ export function createMergeCommand(): Command {
139
137
  onProgress,
140
138
  });
141
139
 
142
- // Clear progress line
143
- progress('', options.quiet);
140
+ clearProgress();
144
141
 
145
- // Output result
146
- console.log(JSON.stringify({
142
+ const output = {
147
143
  success: result.success,
148
144
  transactionHash: result.transactionHash,
149
145
  blockNumber: result.blockNumber,
150
146
  asset: assetUpper,
151
147
  amount: result.amount,
152
148
  type: 'merge',
153
- }, null, 2));
154
- process.exit(0);
149
+ };
150
+
151
+ if (options.json) {
152
+ printJson(output);
153
+ return;
154
+ }
155
+
156
+ printHeader('Merge Submitted');
157
+ printFields([
158
+ { label: 'Asset', value: assetUpper },
159
+ { label: 'Amount', value: result.amount },
160
+ { label: 'Transaction', value: txUrl(result.transactionHash) },
161
+ { label: 'Block', value: result.blockNumber },
162
+ ]);
163
+ printLine();
155
164
  } catch (error) {
156
- progress('', options.quiet);
165
+ clearProgress();
157
166
  handleCLIError(error);
158
167
  }
159
168
  });
@@ -6,26 +6,24 @@ import { Command } from 'commander';
6
6
  import { Keypair } from '../../keypair.js';
7
7
  import { withdraw } from '../../withdraw.js';
8
8
  import { handleCLIError, CLIError, ErrorCode } from '../errors.js';
9
+ import { clearProgress, createProgressReporter, printFields, printHeader, printJson, printLine, txUrl } from '../output.js';
9
10
  import type { RelayPool } from '../../types.js';
10
11
 
11
12
  const SUPPORTED_ASSETS = ['ETH', 'USDC'];
12
13
 
13
- // Progress helper - writes to stderr so JSON output stays clean
14
- function progress(msg: string, quiet?: boolean) {
15
- if (!quiet) {
16
- process.stderr.write(`\r\x1b[K${msg}`);
17
- }
18
- }
19
-
20
14
  export function createWithdrawCommand(): Command {
21
15
  const withdrawCmd = new Command('withdraw')
22
16
  .description('Withdraw from private pool to a public address')
23
17
  .argument('<asset>', 'Asset to withdraw (ETH or USDC)')
24
18
  .argument('<amount>', 'Amount to withdraw (e.g., 0.1)')
25
19
  .argument('<recipient>', 'Recipient address (e.g., 0x...)')
26
- .option('--veil-key <key>', 'Veil private key (or set VEIL_KEY env)')
27
- .option('--rpc-url <url>', 'RPC URL (or set RPC_URL env)')
28
- .option('--quiet', 'Suppress progress output')
20
+ .option('--json', 'Output as JSON')
21
+ .addHelpText('after', `
22
+ Examples:
23
+ veil withdraw ETH 0.05 0xRecipientAddress
24
+ veil withdraw USDC 50 0xRecipientAddress
25
+ veil withdraw ETH 0.05 0xRecipientAddress --json
26
+ `)
29
27
  .action(async (asset: string, amount: string, recipient: string, options) => {
30
28
  try {
31
29
  const assetUpper = asset.toUpperCase();
@@ -41,24 +39,16 @@ export function createWithdrawCommand(): Command {
41
39
  }
42
40
 
43
41
  // Get keypair
44
- const veilKey = options.veilKey || process.env.VEIL_KEY;
42
+ const veilKey = process.env.VEIL_KEY;
45
43
  if (!veilKey) {
46
- throw new CLIError(ErrorCode.VEIL_KEY_MISSING, 'VEIL_KEY required. Use --veil-key or set VEIL_KEY env');
44
+ throw new CLIError(ErrorCode.VEIL_KEY_MISSING, 'VEIL_KEY required. Set VEIL_KEY env');
47
45
  }
48
46
 
49
47
  const keypair = new Keypair(veilKey);
50
- const rpcUrl = options.rpcUrl || process.env.RPC_URL;
48
+ const rpcUrl = process.env.RPC_URL;
51
49
  const pool = assetUpper.toLowerCase() as RelayPool;
52
-
53
- // Progress callback
54
- const onProgress = options.quiet
55
- ? undefined
56
- : (stage: string, detail?: string) => {
57
- const msg = detail ? `${stage}: ${detail}` : stage;
58
- progress(msg, options.quiet);
59
- };
60
-
61
- progress(`Starting ${assetUpper} withdrawal...`, options.quiet);
50
+ const onProgress = createProgressReporter();
51
+ onProgress(`Starting ${assetUpper} withdrawal...`);
62
52
 
63
53
  // Execute withdrawal
64
54
  const result = await withdraw({
@@ -70,21 +60,33 @@ export function createWithdrawCommand(): Command {
70
60
  onProgress,
71
61
  });
72
62
 
73
- // Clear progress line
74
- progress('', options.quiet);
63
+ clearProgress();
75
64
 
76
- // Output result
77
- console.log(JSON.stringify({
65
+ const output = {
78
66
  success: result.success,
79
67
  transactionHash: result.transactionHash,
80
68
  blockNumber: result.blockNumber,
81
69
  asset: assetUpper,
82
70
  amount: result.amount,
83
71
  recipient: result.recipient,
84
- }, null, 2));
85
- process.exit(0);
72
+ };
73
+
74
+ if (options.json) {
75
+ printJson(output);
76
+ return;
77
+ }
78
+
79
+ printHeader('Withdrawal Submitted');
80
+ printFields([
81
+ { label: 'Asset', value: assetUpper },
82
+ { label: 'Amount', value: result.amount },
83
+ { label: 'Recipient', value: result.recipient },
84
+ { label: 'Transaction', value: txUrl(result.transactionHash) },
85
+ { label: 'Block', value: result.blockNumber },
86
+ ]);
87
+ printLine();
86
88
  } catch (error) {
87
- progress('', options.quiet);
89
+ clearProgress();
88
90
  handleCLIError(error);
89
91
  }
90
92
  });
package/src/cli/config.ts CHANGED
@@ -2,22 +2,50 @@
2
2
  * CLI configuration handling
3
3
  */
4
4
 
5
+ import { CLIError, ErrorCode } from './errors.js';
6
+ import { getAddress } from './wallet.js';
5
7
  import type { WalletConfig } from './wallet.js';
6
8
 
7
9
  export interface CLIOptions {
8
- walletKey?: string;
9
10
  rpcUrl?: string;
10
11
  }
11
12
 
13
+ export interface AddressOptions {
14
+ address?: string;
15
+ }
16
+
17
+ export interface ResolvedAddress {
18
+ address: `0x${string}`;
19
+ source: 'flag' | 'wallet-key' | 'signer-address';
20
+ }
21
+
22
+ function validateAddress(address: string, sourceLabel: string): `0x${string}` {
23
+ const normalized = address.trim();
24
+ if (!/^0x[a-fA-F0-9]{40}$/.test(normalized)) {
25
+ throw new CLIError(ErrorCode.INVALID_ADDRESS, `${sourceLabel} must be a valid 0x-prefixed Ethereum address.`);
26
+ }
27
+ return normalized as `0x${string}`;
28
+ }
29
+
30
+ function ensureAddressEnvConsistency(): void {
31
+ if (process.env.WALLET_KEY && process.env.SIGNER_ADDRESS) {
32
+ throw new CLIError(
33
+ ErrorCode.CONFIG_CONFLICT,
34
+ 'WALLET_KEY and SIGNER_ADDRESS are mutually exclusive. Set only one.',
35
+ );
36
+ }
37
+ }
38
+
12
39
  /**
13
40
  * Get wallet configuration from CLI options and environment variables
14
41
  */
15
42
  export function getConfig(options: CLIOptions): WalletConfig {
16
- const walletKey = options.walletKey || process.env.WALLET_KEY;
43
+ ensureAddressEnvConsistency();
44
+ const walletKey = process.env.WALLET_KEY;
17
45
  const rpcUrl = options.rpcUrl || process.env.RPC_URL || 'https://mainnet.base.org';
18
46
 
19
47
  if (!walletKey) {
20
- throw new Error('Wallet key required. Use --wallet-key or set WALLET_KEY environment variable.');
48
+ throw new Error('WALLET_KEY env var required. Set it before running this command.');
21
49
  }
22
50
 
23
51
  // Validate wallet key format
@@ -38,6 +66,58 @@ export function getVeilKey(options: { veilKey?: string }): string | undefined {
38
66
  return options.veilKey || process.env.VEIL_KEY;
39
67
  }
40
68
 
69
+ /**
70
+ * Resolve an address for query / unsigned flows.
71
+ * Prefers explicit --address, then derives from WALLET_KEY, then falls back to SIGNER_ADDRESS.
72
+ */
73
+ export function resolveAddress(
74
+ options: AddressOptions = {},
75
+ config: { required?: boolean; allowInvalidWalletKey?: boolean } = {},
76
+ ): ResolvedAddress | null {
77
+ ensureAddressEnvConsistency();
78
+
79
+ if (options.address) {
80
+ return {
81
+ address: validateAddress(options.address, '--address'),
82
+ source: 'flag',
83
+ };
84
+ }
85
+
86
+ const walletKey = process.env.WALLET_KEY;
87
+ if (walletKey) {
88
+ try {
89
+ return {
90
+ address: getAddress(walletKey as `0x${string}`),
91
+ source: 'wallet-key',
92
+ };
93
+ } catch {
94
+ if (!config.allowInvalidWalletKey) {
95
+ throw new CLIError(
96
+ ErrorCode.WALLET_KEY_MISSING,
97
+ 'Invalid WALLET_KEY format. Must be a 0x-prefixed 64-character hex string.',
98
+ );
99
+ }
100
+ }
101
+ }
102
+
103
+ const signerAddress = process.env.SIGNER_ADDRESS;
104
+ if (signerAddress) {
105
+ return {
106
+ address: validateAddress(signerAddress, 'SIGNER_ADDRESS'),
107
+ source: 'signer-address',
108
+ };
109
+ }
110
+
111
+ if (config.required) {
112
+ throw new CLIError(
113
+ ErrorCode.WALLET_KEY_MISSING,
114
+ 'Must provide --address, set SIGNER_ADDRESS, or set WALLET_KEY env.',
115
+ );
116
+ }
117
+
118
+ return null;
119
+ }
120
+
41
121
  /**
42
122
  * Load environment variables from .env.veil and .env files
43
123
  */
@@ -46,10 +126,10 @@ export function loadEnv(): void {
46
126
  // Dynamic import to avoid bundling issues
47
127
  // eslint-disable-next-line @typescript-eslint/no-require-imports
48
128
  const dotenv = require('dotenv');
49
-
129
+
50
130
  // Load .env.veil first (Veil-specific config)
51
131
  dotenv.config({ path: '.env.veil', quiet: true });
52
-
132
+
53
133
  // Then load .env (for WALLET_KEY, RPC_URL if not in .env.veil)
54
134
  dotenv.config({ quiet: true });
55
135
  } catch {
package/src/cli/errors.ts CHANGED
@@ -9,6 +9,7 @@ export const ErrorCode = {
9
9
  VEIL_KEY_MISSING: 'VEIL_KEY_MISSING',
10
10
  WALLET_KEY_MISSING: 'WALLET_KEY_MISSING',
11
11
  DEPOSIT_KEY_MISSING: 'DEPOSIT_KEY_MISSING',
12
+ CONFIG_CONFLICT: 'CONFIG_CONFLICT',
12
13
  INVALID_ADDRESS: 'INVALID_ADDRESS',
13
14
  INVALID_AMOUNT: 'INVALID_AMOUNT',
14
15
  INSUFFICIENT_BALANCE: 'INSUFFICIENT_BALANCE',
@@ -51,6 +52,9 @@ function inferErrorCode(message: string): ErrorCodeType {
51
52
  if (msg.includes('deposit_key') || msg.includes('deposit key')) {
52
53
  return ErrorCode.DEPOSIT_KEY_MISSING;
53
54
  }
55
+ if (msg.includes('mutually exclusive') || msg.includes('config conflict') || msg.includes('signer_address')) {
56
+ return ErrorCode.CONFIG_CONFLICT;
57
+ }
54
58
  if (msg.includes('invalid') && msg.includes('address')) {
55
59
  return ErrorCode.INVALID_ADDRESS;
56
60
  }
package/src/cli/index.ts CHANGED
@@ -8,8 +8,8 @@
8
8
  * veil register # Register on-chain
9
9
  * veil deposit ETH 0.1 # Deposit ETH
10
10
  * veil balance # Show all balances
11
- * veil queue-balance # Show pending queue deposits
12
- * veil private-balance # Show private balance
11
+ * veil balance queue # Show pending queue deposits
12
+ * veil balance private # Show private balance
13
13
  * veil withdraw ETH 0.1 0x... # Withdraw to public address
14
14
  * veil transfer ETH 0.1 0x... # Transfer privately
15
15
  * veil merge ETH 0.5 # Merge UTXOs (self-transfer)
@@ -36,7 +36,14 @@ const program = new Command();
36
36
  program
37
37
  .name('veil')
38
38
  .description('CLI for Veil Cash privacy pools on Base')
39
- .version('0.3.0');
39
+ .version('0.5.0')
40
+ .addHelpText('after', `
41
+ Getting started:
42
+ veil init
43
+ veil register
44
+ veil deposit ETH 0.1
45
+ veil balance
46
+ `);
40
47
 
41
48
  // Add commands
42
49
  program.addCommand(createInitCommand());
@@ -44,12 +51,23 @@ program.addCommand(createKeypairCommand());
44
51
  program.addCommand(createRegisterCommand());
45
52
  program.addCommand(createDepositCommand());
46
53
  program.addCommand(createBalanceCommand());
47
- program.addCommand(createQueueBalanceCommand());
48
- program.addCommand(createPrivateBalanceCommand());
54
+ program.addCommand(createQueueBalanceCommand(), { hidden: true });
55
+ program.addCommand(createPrivateBalanceCommand(), { hidden: true });
49
56
  program.addCommand(createWithdrawCommand());
50
57
  program.addCommand(createTransferCommand());
51
58
  program.addCommand(createMergeCommand());
52
59
  program.addCommand(createStatusCommand());
53
60
 
61
+ const knownTopLevelCommands = new Set([
62
+ ...program.commands.map((command) => command.name()),
63
+ 'help',
64
+ ]);
65
+
66
+ const firstArg = process.argv[2];
67
+ if (firstArg && !firstArg.startsWith('-') && !knownTopLevelCommands.has(firstArg)) {
68
+ console.error(`error: unknown command '${firstArg}'`);
69
+ process.exit(1);
70
+ }
71
+
54
72
  // Parse and execute
55
73
  program.parse();
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Shared CLI output helpers.
3
+ */
4
+
5
+ const DIM = '\x1b[2m';
6
+ const BOLD = '\x1b[1m';
7
+ const RESET = '\x1b[0m';
8
+
9
+ export interface JsonOutputOption {
10
+ json?: boolean;
11
+ }
12
+
13
+ export function printJson(value: unknown): void {
14
+ console.log(JSON.stringify(value, null, 2));
15
+ }
16
+
17
+ export function printLine(value = ''): void {
18
+ console.log(value);
19
+ }
20
+
21
+ export function printHeader(title: string): void {
22
+ console.log();
23
+ console.log(`${BOLD}${title}${RESET}`);
24
+ console.log(`${DIM}${'─'.repeat(40)}${RESET}`);
25
+ }
26
+
27
+ export function printSection(title: string): void {
28
+ console.log();
29
+ console.log(`${BOLD}${title}${RESET}`);
30
+ }
31
+
32
+ export function printDivider(): void {
33
+ console.log();
34
+ console.log(`${DIM}${'─'.repeat(40)}${RESET}`);
35
+ }
36
+
37
+ export function printFields(fields: Array<{ label: string; value: unknown }>): void {
38
+ const visibleFields = fields.filter((field) => field.value !== undefined);
39
+ const width = visibleFields.reduce((max, field) => Math.max(max, field.label.length), 0);
40
+
41
+ for (const field of visibleFields) {
42
+ const label = `${DIM}${field.label.padEnd(width)}${RESET}`;
43
+ console.log(` ${label} ${formatValue(field.value)}`);
44
+ }
45
+ }
46
+
47
+ export function printList(items: string[]): void {
48
+ if (items.length === 0) {
49
+ console.log(` ${DIM}(none)${RESET}`);
50
+ return;
51
+ }
52
+
53
+ for (const item of items) {
54
+ console.log(` ${item}`);
55
+ }
56
+ }
57
+
58
+ export function maskValue(value: string, start = 10, end = 8): string {
59
+ if (value.length <= start + end + 3) {
60
+ return value;
61
+ }
62
+
63
+ return `${value.slice(0, start)}...${value.slice(-end)}`;
64
+ }
65
+
66
+ export function txUrl(hash: string): string {
67
+ return `https://basescan.org/tx/${hash}`;
68
+ }
69
+
70
+ export function clearProgress(): void {
71
+ process.stderr.write('\r\x1b[K');
72
+ }
73
+
74
+ export function createProgressReporter(): (stage: string, detail?: string) => void {
75
+ return (stage: string, detail?: string) => {
76
+ const message = detail ? `${stage}: ${detail}` : stage;
77
+ process.stderr.write(`\r\x1b[K${message}`);
78
+ };
79
+ }
80
+
81
+ function formatValue(value: unknown): string {
82
+ if (value === null) return 'null';
83
+ if (value === undefined) return '';
84
+ if (typeof value === 'boolean') return value ? 'yes' : 'no';
85
+ if (typeof value === 'object') return JSON.stringify(value);
86
+ return String(value);
87
+ }