Skill: bevanding/walletconnect-requester (version 1.0.2)

--- name: walletconnect-requester description: Secure WalletConnect integration for AI agents. Connect to user wallets as a DApp (Proposer) without ever handling private keys. Request transactions and signatures - users approve everything in their wallet. Zero custody, maximum security. Use when you need to interact with user wallets securely via WalletConnect v2. metadata: openclaw: requires: env: - WC_PROJECT_ID (required): WalletConnect Cloud Project ID - WC_METADATA_NAME (optional): DApp name shown in wallet - WC_METADATA_URL (optional): DApp URL - WC_METADATA_ICONS (optional): DApp icon URLs (comma-separated) persistence: path: ~/.walletconnect-requester/ files: - sessions.json: Active WalletConnect sessions - audit.log: Transaction audit log (masked sensitive data) security_notes: - WalletConnect URI contains symmetric key (symKey) - treat as sensitive - Session tokens grant transaction request capability - protect accordingly - Audit logs contain masked addresses and tx hashes - review before sharing --- # WalletConnect Requester > **Zero custody. Maximum security. User always in control.** ## Why This Skill? Unlike `walletconnect-agent` which **holds private keys and auto-signs**, this skill takes a fundamentally different approach: | | walletconnect-agent | walletconnect-requester (this skill) | |---|---|---| | **Private Keys** | ⚠️ Stored in agent | ✅ Never touches agent | | **Signing** | ⚠️ Auto-signs everything | ✅ User approves each tx | | **Security Model** | Custodial (agent has full control) | **Non-custodial (user has full control)** | | **If Agent is Compromised** | ⚠️ Funds can be stolen | ✅ Funds are safe - no keys to steal | **This is the safest way for AI agents to interact with Web3.** ## What This Does - **Connect to user wallets** via WalletConnect v2 - **Request transactions** - user approves in their wallet - **Request signatures** - user signs in their wallet - **Zero private key exposure** - keys never leave the user's wallet ## Security Guarantees ``` ┌─────────────────┐ ┌─────────────────┐ │ AI Agent │ │ User Wallet │ │ (Requester) │ ◄── WalletConnect ──► │ (Signer) │ │ │ Session │ │ └─────────────────┘ └─────────────────┘ │ │ │ 1. Request transaction │ │ ─────────────────────────────────► │ │ │ │ 2. User reviews & approves │ │ (in wallet UI) │ │ │ │ 3. Signed transaction │ │ ◄───────────────────────────────── │ │ │ ▼ ▼ NO PRIVATE KEYS PRIVATE KEYS NO AUTO-SIGN USER APPROVES USER IN CONTROL EVERYTHING ``` ## Installation ### Step 1: Install Dependencies This skill requires Node.js dependencies. Install them globally or locally: ```bash # Install dependencies npm install @walletconnect/sign-client @walletconnect/core qrcode ``` ### Step 2: Get WalletConnect Project ID 1. Go to [WalletConnect Cloud](https://cloud.walletconnect.com/) 2. Create a new project 3. Copy your **Project ID** ### Step 3: Set Environment Variable ```bash export WC_PROJECT_ID="your_project_id_here" ``` ### Step 4: Run the Skill ```bash node scripts/wc-requester.js connect ``` --- ## Quick Start ### Step 1: Create a Session ```bash export WC_PROJECT_ID="your_project_id" node scripts/wc-requester.js connect ``` Output: ``` WalletConnect URI: wc:abc123...@2?relay-protocol=irn&symKey=xyz Scan this QR code with your wallet: [QR CODE] Waiting for wallet to connect... ``` ### Step 2: Request a Transaction ```bash node scripts/wc-requester.js request-tx \ --to 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 \ --data 0xa9059cbb... \ --value 0 \ --chain 8453 ``` User sees in wallet: ``` Send 10 USDC to 0x1F3A...? [Approve] [Reject] ``` ### Step 3: Request a Signature ```bash node scripts/wc-requester.js request-sign \ --message "Sign this message to verify ownership" \ --chain 8453 ``` ## Commands ### `connect` - Create WalletConnect Session ```bash node scripts/wc-requester.js connect [options] Options: --chains <ids> Comma-separated chain IDs (default: 8453,1) --methods <list> Comma-separated methods (default: eth_sendTransaction,personal_sign) --qr <path> Generate QR code to file --json Output as JSON ``` ### `request-tx` - Request Transaction ```bash node scripts/wc-requester.js request-tx --to <address> --data <hex> --value <wei> --chain <id> ``` ### `request-sign` - Request Signature ```bash node scripts/wc-requester.js request-sign --message <text> --chain <id> # or for typed data node scripts/wc-requester.js request-sign --typed-data <json> --chain <id> ``` ### `sessions` - List Active Sessions ```bash node scripts/wc-requester.js sessions ``` ### `disconnect` - End Session ```bash node scripts/wc-requester.js disconnect --topic <topic> ``` ## Security Model ### What Agent CAN Do - ✅ Request transactions (user must approve) - ✅ Request signatures (user must approve) - ✅ View connected wallet address - ✅ View session metadata ### What Agent CANNOT Do - ❌ Hold private keys - ❌ Auto-sign anything - ❌ Execute transactions without approval - ❌ Access funds directly ### If Agent is Compromised - ✅ Attacker cannot steal funds (no keys) - ✅ Attacker cannot auto-sign transactions - ✅ User can reject any suspicious request - ✅ User can disconnect session anytime ### Local Data Persistence This skill writes files to `~/.walletconnect-requester/`: | File | Purpose | Sensitivity | |------|---------|-------------| | `sessions.json` | Active WalletConnect sessions | ⚠️ Contains session topics | | `audit.log` | Transaction audit log | ⚠️ Contains masked tx hashes | **Security recommendations:** - Review `audit.log` before sharing - Delete `sessions.json` when no longer needed - Set appropriate file permissions: `chmod 600 ~/.walletconnect-requester/*` ### Sensitive Data Handling | Data Type | How It's Handled | |-----------|------------------| | **WalletConnect URI** | Contains `symKey` - displayed once during connection, not logged | | **Session tokens** | Stored locally in `sessions.json`, not transmitted externally | | **Transaction hashes** | Logged in `audit.log` with masked addresses | | **Private keys** | ❌ Never handled by this skill | ### Privacy Considerations - WalletConnect URI (with `symKey`) is printed to stdout for QR code generation - Audit logs mask full addresses (e.g., `0x8335...` instead of full address) - No data is sent to external servers except WalletConnect relay network ## Configuration ### Environment Variables | Variable | Required | Description | |----------|----------|-------------| | `WC_PROJECT_ID` | Yes | WalletConnect Cloud Project ID | | `WC_METADATA_NAME` | No | DApp name shown in wallet | | `WC_METADATA_URL` | No | DApp URL | | `WC_METADATA_ICONS` | No | DApp icon URL | ### Namespaces Configuration The skill requests minimal permissions by default: ```json { "eip155": { "chains": ["eip155:8453", "eip155:1"], "methods": ["eth_sendTransaction", "personal_sign"], "events": ["accountsChanged", "chainChanged"] } } ``` ## Example Workflows ### Connect and Request Payment ```bash # 1. Create session node scripts/wc-requester.js connect --qr /tmp/qr.png # User scans QR with MetaMask # 2. Request USDC transfer node scripts/wc-requester.js request-tx \ --to 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 \ --data 0xa9059cbb0000... \ --chain 8453 # User sees: "Send 10 USDC?" → Approves in wallet # Returns: tx_hash ``` ### Verify Wallet Ownership ```bash # 1. Connect node scripts/wc-requester.js connect # 2. Request signature node scripts/wc-requester.js request-sign \ --message "I own this wallet on March 9, 2026" # User signs in wallet # Returns: signature ``` ## Comparison with Other Solutions | Feature | This Skill | walletconnect-agent | MetaMask SDK | |---------|-----------|---------------------|--------------| | Private Key Storage | ❌ Never | ⚠️ In Agent | ❌ Never | | Auto-sign | ❌ Never | ✅ Yes | ❌ No | | User Approval Required | ✅ Always | ❌ No | ✅ Always | | Multi-wallet Support | ✅ Any WC wallet | ✅ Any WC wallet | ❌ MetaMask only | | Security Level | **Highest** | Medium | High | | Best For | User-controlled tx | Automated trading | MetaMask users | ## Supported Wallets Any wallet that supports WalletConnect v2: - MetaMask Mobile - Rainbow - Trust Wallet - Coinbase Wallet - Ledger Live - And 500+ more... ## Troubleshooting ### "No active session" Run `connect` first to create a session. ### "User rejected request" User declined in their wallet. Ask if they want to retry. ### "Session expired" Sessions last 7 days by default. Reconnect to create a new session. ### "Unsupported chain" User's wallet doesn't support the requested chain. Ask them to switch networks. ## Audit Log All requests are logged (without sensitive data): ``` ~/.walletconnect-requester/audit.log { "timestamp": "2026-03-09T02:00:00Z", "action": "request_transaction", "chain": 8453, "to": "0x8335...", "status": "approved", "tx_hash": "0xabc123..." } ``` ## When to Use This vs walletconnect-agent | Use This Skill When | Use walletconnect-agent When | |--------------------|------------------------------| | User must approve every tx | Fully automated trading | | Maximum security required | You trust the agent completely | | One-time or occasional tx | 24/7 unattended operation | | User wants full control | User wants set-and-forget | | Agent runs in untrusted env | Agent runs in secure env | **When in doubt, use this skill.** It's always safer. --- ## License MIT — Built with security as the #1 priority. --- **Maintainer**: Web3 Investor Team **Registry**: https://clawhub.com/skills/walletconnect-requester

path: CHANGELOG.md
content: # Changelog

## [1.0.0] - 2026-03-09

### Added
- Initial release
- Non-custodial WalletConnect v2 client for AI agents
- Session management (connect, list, disconnect)
- Transaction requests (user must approve)
- Signature requests (user must approve)
- QR code generation for easy wallet scanning
- Audit logging (without sensitive data)
- Multi-chain support (Base, Ethereum, Arbitrum, Optimism, Polygon)

### Security
- **Zero private key exposure** - Keys never leave user wallet
- **User approval required** - Every transaction must be approved
- **`eth_sign` blocked by default** - Prevents phishing attacks
- **Audit trail** - All requests logged with masked sensitive data

### Differences from walletconnect-agent

| Feature | walletconnect-agent | walletconnect-requester |
|---------|---------------------|------------------------|
| Private keys | Stored in agent | **Never in agent** |
| Auto-sign | Yes | **No - user approves** |
| Security model | Custodial | **Non-custodial** |
| If compromised | Funds at risk | **Funds safe** |

path: config/namespaces.json
content: {
  "default": {
    "eip155": {
      "chains": ["eip155:8453", "eip155:1"],
      "methods": ["eth_sendTransaction", "personal_sign", "eth_signTypedData_v4"],
      "events": ["accountsChanged", "chainChanged"]
    }
  },
  "minimal": {
    "eip155": {
      "chains": ["eip155:8453"],
      "methods": ["personal_sign"],
      "events": []
    }
  },
  "full": {
    "eip155": {
      "chains": ["eip155:1", "eip155:8453", "eip155:42161", "eip155:10", "eip155:137"],
      "methods": [
        "eth_sendTransaction",
        "personal_sign",
        "eth_signTypedData_v4",
        "eth_signTransaction"
      ],
      "events": ["accountsChanged", "chainChanged"]
    }
  }
}

path: references/SECURITY.md
content: # Security Model

## Overview

WalletConnect Requester implements a **non-custodial** security model where the AI agent never handles private keys.

## Key Principles

### 1. Zero Private Key Exposure

```
┌─────────────────┐                    ┌─────────────────┐
│   AI Agent      │                    │   User Wallet   │
│   (Requester)   │ ◄──────────────► │   (Signer)      │
│                 │   WalletConnect    │                 │
│  NO PRIVATE     │      v2            │  PRIVATE KEYS   │
│  KEYS EVER      │                    │  STORED HERE    │
└─────────────────┘                    └─────────────────┘
```

### 2. Every Transaction Requires User Approval

| Action | Agent Can Do | User Must Approve |
|--------|-------------|-------------------|
| Request transaction | ✅ | ✅ (in wallet) |
| Request signature | ✅ | ✅ (in wallet) |
| View wallet address | ✅ | ❌ |
| Execute transaction | ❌ | User only |
| Sign message | ❌ | User only |

### 3. Session-Based Access Control

- Sessions expire after 7 days (WalletConnect default)
- User can disconnect session anytime
- Agent cannot force any action

## Attack Scenarios

### Scenario: Agent is Compromised

```
Attacker gains control of the AI agent
         ↓
Attacker tries to steal funds
         ↓
Attacker CANNOT:
  - Access private keys (agent never has them)
  - Auto-sign transactions (not possible)
  - Execute transactions without approval
         ↓
Attacker CAN ONLY:
  - Request transactions (user must approve)
  - Request signatures (user must approve)
  - View wallet address (non-sensitive)
         ↓
Result: FUNDS ARE SAFE
```

### Scenario: Session Token Stolen

```
Attacker obtains session token
         ↓
Attacker can only:
  - Request transactions (user still must approve)
  - Cannot auto-sign
  - Cannot access private keys
         ↓
Result: FUNDS ARE SAFE
```

## Comparison with Custodial Solutions

| Risk | walletconnect-agent (Custodial) | walletconnect-requester (Non-Custodial) |
|------|--------------------------------|----------------------------------------|
| Private key theft | ⚠️ All funds lost | ✅ Impossible - no keys |
| Agent compromised | ⚠️ Attacker can drain funds | ✅ User must approve |
| Malicious code | ⚠️ Can steal keys | ✅ Can only request |
| Session hijack | ⚠️ Auto-sign available | ✅ Still needs approval |

## Audit Trail

All requests are logged to `~/.walletconnect-requester/audit.log`:

```json
{
  "timestamp": "2026-03-09T02:00:00Z",
  "action": "transaction_request",
  "chain": 8453,
  "to": "0x8335...",
  "status": "approved",
  "tx_hash": "0xabc123..."
}
```

**Sensitive data is masked** - full addresses and keys are never logged.

## Best Practices

### For Users

1. **Review every request** - Check transaction details before approving
2. **Use dedicated wallet** - Separate wallet for agent interactions
3. **Disconnect when done** - End sessions when not needed
4. **Check audit logs** - Review `~/.walletconnect-requester/audit.log`

### For Developers

1. **Never ask for private keys** - This defeats the purpose
2. **Display transaction details** - Show user what they're signing
3. **Implement rate limiting** - Don't spam user with requests
4. **Handle rejections gracefully** - User may decline any request

## Supported Methods

### Safe Methods (Always Allowed)

| Method | Risk Level | Description |
|--------|-----------|-------------|
| `personal_sign` | ✅ Low | User signs readable message |
| `eth_signTypedData_v4` | ✅ Low | User signs structured data |
| `eth_sendTransaction` | ⚠️ Medium | User must verify transaction |

### Blocked Methods

| Method | Reason |
|--------|--------|
| `eth_sign` | ⚠️ Dangerous - allows signing arbitrary data |

**Why `eth_sign` is blocked:** This method allows signing any arbitrary data, which can be used in phishing attacks to trick users into signing transactions. Use `personal_sign` instead.

## Network Security

- All communication is end-to-end encrypted (WalletConnect v2)
- Messages relay through decentralized Waku network
- Private keys never transmitted

## Conclusion

**This skill is designed for maximum security.** By never handling private keys and requiring user approval for every action, it provides the safest way for AI agents to interact with Web3.

path: scripts/package.json
content: {
  "name": "walletconnect-requester",
  "version": "1.0.0",
  "description": "Secure WalletConnect client for AI agents - Zero custody, user controlled",
  "main": "scripts/wc-requester.js",
  "bin": {
    "wc-requester": "./scripts/wc-requester.js"
  },
  "scripts": {
    "connect": "node scripts/wc-requester.js connect",
    "sessions": "node scripts/wc-requester.js sessions"
  },
  "dependencies": {
    "@walletconnect/sign-client": "^2.13.0",
    "@walletconnect/core": "^2.13.0",
    "qrcode": "^1.5.3"
  },
  "keywords": [
    "walletconnect",
    "web3",
    "ai-agent",
    "non-custodial",
    "secure"
  ],
  "license": "MIT"
}

path: scripts/wc-requester.js
content: #!/usr/bin/env node
/**
 * WalletConnect Requester - Secure DApp Client for AI Agents
 * 
 * This script implements WalletConnect v2 as a DApp (Proposer),
 * meaning it NEVER handles private keys. All transactions must be
 * approved by the user in their wallet.
 * 
 * Security Model:
 * - Agent acts as DApp (Requester)
 * - User wallet acts as Signer (Responder)
 * - Private keys never leave user's wallet
 * - Every transaction requires user approval
 * 
 * Usage:
 *   node wc-requester.js connect
 *   node wc-requester.js request-tx --to 0x... --data 0x... --chain 8453
 *   node wc-requester.js request-sign --message "hello" --chain 8453
 *   node wc-requester.js sessions
 *   node wc-requester.js disconnect --topic <topic>
 */

const { SignClient } = require('@walletconnect/sign-client');
const { WalletConnectError } = require('@walletconnect/utils');
const readline = require('readline');
const fs = require('fs');
const path = require('path');
const QRCode = require('qrcode');

// ============================================================================
// Configuration
// ============================================================================

const CONFIG_DIR = path.join(process.env.HOME, '.walletconnect-requester');
const SESSIONS_FILE = path.join(CONFIG_DIR, 'sessions.json');
const AUDIT_LOG = path.join(CONFIG_DIR, 'audit.log');

// Default metadata
const DEFAULT_METADATA = {
  name: process.env.WC_METADATA_NAME || 'AI Agent Requester',
  description: 'Secure WalletConnect client for AI agents - Zero custody, user controlled',
  url: process.env.WC_METADATA_URL || 'https://github.com/openclaw',
  icons: process.env.WC_METADATA_ICONS?.split(',') || ['https://avatars.githubusercontent.com/u/1234567']
};

// Default namespaces
const DEFAULT_NAMESPACES = {
  eip155: {
    chains: ['eip155:8453', 'eip155:1'],  // Base, Ethereum
    methods: ['eth_sendTransaction', 'personal_sign', 'eth_signTypedData_v4'],
    events: ['accountsChanged', 'chainChanged']
  }
};

// ============================================================================
// Utilities
// ============================================================================

function ensureConfigDir() {
  if (!fs.existsSync(CONFIG_DIR)) {
    fs.mkdirSync(CONFIG_DIR, { recursive: true });
  }
}

function loadSessions() {
  ensureConfigDir();
  if (fs.existsSync(SESSIONS_FILE)) {
    return JSON.parse(fs.readFileSync(SESSIONS_FILE, 'utf8'));
  }
  return {};
}

function saveSessions(sessions) {
  ensureConfigDir();
  fs.writeFileSync(SESSIONS_FILE, JSON.stringify(sessions, null, 2));
}

function logAudit(entry) {
  ensureConfigDir();
  const logEntry = {
    timestamp: new Date().toISOString(),
    ...entry
  };
  fs.appendFileSync(AUDIT_LOG, JSON.stringify(logEntry) + '\n');
}

function maskAddress(address) {
  if (!address) return address;
  return `${address.slice(0, 6)}...${address.slice(-4)}`;
}

async function generateQRCode(uri, outputPath) {
  await QRCode.toFile(outputPath, uri, {
    width: 400,
    margin: 2
  });
  return outputPath;
}

// ============================================================================
// WalletConnect Client
// ============================================================================

class WalletConnectRequester {
  constructor() {
    this.client = null;
    this.projectId = process.env.WC_PROJECT_ID;
    
    if (!this.projectId) {
      console.error('Error: WC_PROJECT_ID environment variable is required.');
      console.error('Get your project ID at: https://cloud.walletconnect.com');
      process.exit(1);
    }
  }

  async init() {
    this.client = await SignClient.init({
      projectId: this.projectId,
      metadata: DEFAULT_METADATA
    });

    // Set up event listeners
    this.client.on('session_delete', ({ topic }) => {
      const sessions = loadSessions();
      delete sessions[topic];
      saveSessions(sessions);
      console.log(`\nSession ${maskAddress(topic)} disconnected by user.`);
    });

    this.client.on('session_expire', ({ topic }) => {
      const sessions = loadSessions();
      delete sessions[topic];
      saveSessions(sessions);
      console.log(`\nSession ${maskAddress(topic)} expired.`);
    });

    return this.client;
  }

  async connect(options = {}) {
    const chains = options.chains || ['8453', '1'];
    const methods = options.methods || ['eth_sendTransaction', 'personal_sign'];
    
    // Build namespaces
    const namespaces = {
      eip155: {
        chains: chains.map(id => `eip155:${id}`),
        methods: methods,
        events: ['accountsChanged', 'chainChanged']
      }
    };

    const { uri, approval } = await this.client.connect({
      requiredNamespaces: namespaces
    });

    console.log('\n' + '='.repeat(60));
    console.log('WalletConnect URI:');
    console.log('='.repeat(60));
    console.log(uri);
    console.log('='.repeat(60));

    // Generate QR code if requested
    if (options.qr) {
      await generateQRCode(uri, options.qr);
      console.log(`\nQR code saved to: ${options.qr}`);
    }

    console.log('\n📱 Scan this URI with your wallet (MetaMask, Rainbow, etc.)');
    console.log('⏳ Waiting for connection...\n');

    try {
      const session = await approval();
      
      // Save session
      const sessions = loadSessions();
      sessions[session.topic] = {
        topic: session.topic,
        peer: session.peer.metadata,
        accounts: session.namespaces.eip155.accounts,
        expiry: session.expiry,
        connectedAt: new Date().toISOString()
      };
      saveSessions(sessions);

      console.log('✅ Connected successfully!');
      console.log(`   Wallet: ${session.peer.metadata.name}`);
      console.log(`   Accounts: ${session.namespaces.eip155.accounts.map(a => maskAddress(a.split(':')[2])).join(', ')}`);
      
      logAudit({
        action: 'session_connected',
        topic: session.topic,
        peer: session.peer.metadata.name
      });

      return session;
    } catch (error) {
      console.error('❌ Connection failed:', error.message);
      logAudit({
        action: 'session_connect_failed',
        error: error.message
      });
      throw error;
    }
  }

  async requestTransaction(options) {
    const { to, data, value, chainId, from } = options;
    const sessions = loadSessions();
    const sessionTopics = Object.keys(sessions);

    if (sessionTopics.length === 0) {
      console.error('❌ No active session. Run "connect" first.');
      return null;
    }

    // Use first active session (or prompt if multiple)
    let topic = sessionTopics[0];
    if (sessionTopics.length > 1) {
      console.log('Multiple sessions available. Use --topic to specify.');
      sessionTopics.forEach((t, i) => {
        console.log(`  ${i + 1}. ${sessions[t].peer?.name || maskAddress(t)}`);
      });
      return null;
    }

    const session = this.client.session.get(topic);
    if (!session) {
      console.error('❌ Session not found. It may have expired.');
      delete sessions[topic];
      saveSessions(sessions);
      return null;
    }

    // Get the account for the specified chain
    const chainAccounts = session.namespaces.eip155.accounts
      .filter(a => a.startsWith(`eip155:${chainId}:`));
    
    if (chainAccounts.length === 0) {
      console.error(`❌ No account available for chain ${chainId}`);
      return null;
    }

    const accountAddress = from || chainAccounts[0].split(':')[2];

    const tx = {
      from: accountAddress,
      to: to,
      data: data || '0x',
      value: value || '0x0'
    };

    console.log('\n📤 Requesting transaction...');
    console.log(`   Chain: ${chainId}`);
    console.log(`   From: ${maskAddress(tx.from)}`);
    console.log(`   To: ${maskAddress(tx.to)}`);
    console.log(`   Value: ${tx.value}`);
    console.log(`   Data: ${tx.data.slice(0, 30)}...`);
    console.log('\n⏳ Waiting for user approval in wallet...\n');

    logAudit({
      action: 'transaction_request',
      topic: topic,
      chain: chainId,
      to: maskAddress(to),
      value: value
    });

    try {
      const result = await this.client.request({
        topic: topic,
        chainId: `eip155:${chainId}`,
        request: {
          method: 'eth_sendTransaction',
          params: [tx]
        }
      });

      console.log('✅ Transaction approved and submitted!');
      console.log(`   TX Hash: ${result}`);
      
      logAudit({
        action: 'transaction_approved',
        topic: topic,
        tx_hash: result
      });

      return result;
    } catch (error) {
      if (error.message?.includes('User rejected')) {
        console.log('❌ User rejected the transaction.');
        logAudit({
          action: 'transaction_rejected',
          topic: topic
        });
      } else {
        console.error('❌ Transaction failed:', error.message);
        logAudit({
          action: 'transaction_failed',
          topic: topic,
          error: error.message
        });
      }
      throw error;
    }
  }

  async requestSignature(options) {
    const { message, typedData, chainId, from } = options;
    const sessions = loadSessions();
    const sessionTopics = Object.keys(sessions);

    if (sessionTopics.length === 0) {
      console.error('❌ No active session. Run "connect" first.');
      return null;
    }

    let topic = sessionTopics[0];
    const session = this.client.session.get(topic);
    
    if (!session) {
      console.error('❌ Session not found.');
      return null;
    }

    const chainAccounts = session.namespaces.eip155.accounts
      .filter(a => a.startsWith(`eip155:${chainId}:`));
    
    if (chainAccounts.length === 0) {
      console.error(`❌ No account available for chain ${chainId}`);
      return null;
    }

    const accountAddress = from || chainAccounts[0].split(':')[2];

    console.log('\n📤 Requesting signature...');
    console.log(`   Chain: ${chainId}`);
    console.log(`   Account: ${maskAddress(accountAddress)}`);
    if (message) {
      console.log(`   Message: ${message.slice(0, 50)}${message.length > 50 ? '...' : ''}`);
    }
    console.log('\n⏳ Waiting for user approval in wallet...\n');

    logAudit({
      action: 'signature_request',
      topic: topic,
      chain: chainId,
      account: maskAddress(accountAddress)
    });

    try {
      let result;
      
      if (typedData) {
        result = await this.client.request({
          topic: topic,
          chainId: `eip155:${chainId}`,
          request: {
            method: 'eth_signTypedData_v4',
            params: [accountAddress, typedData]
          }
        });
      } else {
        result = await this.client.request({
          topic: topic,
          chainId: `eip155:${chainId}`,
          request: {
            method: 'personal_sign',
            params: [message, accountAddress]
          }
        });
      }

      console.log('✅ Signature obtained!');
      console.log(`   Signature: ${result.slice(0, 30)}...`);
      
      logAudit({
        action: 'signature_approved',
        topic: topic
      });

      return result;
    } catch (error) {
      if (error.message?.includes('User rejected')) {
        console.log('❌ User rejected the signature request.');
        logAudit({
          action: 'signature_rejected',
          topic: topic
        });
      } else {
        console.error('❌ Signature failed:', error.message);
      }
      throw error;
    }
  }

  listSessions() {
    const sessions = loadSessions();
    const topics = Object.keys(sessions);

    if (topics.length === 0) {
      console.log('No active sessions.');
      console.log('Run "connect" to create a new session.');
      return;
    }

    console.log('\n' + '='.repeat(60));
    console.log('Active Sessions');
    console.log('='.repeat(60));

    topics.forEach((topic, i) => {
      const session = sessions[topic];
      console.log(`\n${i + 1}. Topic: ${maskAddress(topic)}`);
      console.log(`   Wallet: ${session.peer?.name || 'Unknown'}`);
      console.log(`   Accounts: ${session.accounts?.map(a => maskAddress(a.split(':')[2])).join(', ')}`);
      console.log(`   Connected: ${session.connectedAt}`);
      console.log(`   Expires: ${new Date(session.expiry * 1000).toISOString()}`);
    });
    console.log('\n' + '='.repeat(60));
  }

  async disconnect(topic) {
    const sessions = loadSessions();

    if (!topic) {
      const topics = Object.keys(sessions);
      if (topics.length === 0) {
        console.log('No active sessions to disconnect.');
        return;
      }
      topic = topics[0];
      console.log(`Disconnecting: ${maskAddress(topic)}`);
    }

    try {
      await this.client.disconnect({
        topic: topic,
        reason: {
          code: 6000,
          message: 'User disconnected'
        }
      });
    } catch (e) {
      // Session might already be disconnected
    }

    delete sessions[topic];
    saveSessions(sessions);

    console.log('✅ Session disconnected.');
    logAudit({
      action: 'session_disconnected',
      topic: topic
    });
  }
}

// ============================================================================
// CLI
// ============================================================================

function printHelp() {
  console.log(`
WalletConnect Requester - Secure DApp Client for AI Agents

Usage:
  node wc-requester.js <command> [options]

Commands:
  connect               Create a new WalletConnect session
  request-tx            Request a transaction (user must approve)
  request-sign          Request a signature (user must approve)
  sessions              List active sessions
  disconnect            End a session

Options:
  connect:
    --chains <ids>      Comma-separated chain IDs (default: 8453,1)
    --methods <list>    Comma-separated methods (default: eth_sendTransaction,personal_sign)
    --qr <path>         Generate QR code to file
    --json              Output as JSON

  request-tx:
    --to <address>      Recipient address (required)
    --data <hex>        Transaction data (default: 0x)
    --value <wei>       Value in wei (default: 0)
    --chain <id>        Chain ID (default: 8453)
    --from <address>    Sender address (optional, uses first available)

  request-sign:
    --message <text>    Message to sign (for personal_sign)
    --typed-data <json> Typed data to sign (for eth_signTypedData_v4)
    --chain <id>        Chain ID (default: 8453)
    --from <address>    Signer address (optional)

  disconnect:
    --topic <topic>     Session topic to disconnect (optional, uses first)

Environment Variables:
  WC_PROJECT_ID         WalletConnect Cloud Project ID (required)
  WC_METADATA_NAME      DApp name shown in wallet
  WC_METADATA_URL       DApp URL
  WC_METADATA_ICONS     DApp icon URLs (comma-separated)

Examples:
  # Connect to wallet
  WC_PROJECT_ID=xxx node wc-requester.js connect --qr wallet.png

  # Request USDC transfer
  node wc-requester.js request-tx \\
    --to 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 \\
    --data 0xa9059cbb... \\
    --chain 8453

  # Request signature
  node wc-requester.js request-sign \\
    --message "Verify ownership" \\
    --chain 1
`);
}

async function main() {
  const args = process.argv.slice(2);
  const command = args[0];

  if (!command || command === '--help' || command === '-h') {
    printHelp();
    process.exit(0);
  }

  // Parse options
  const options = {};
  for (let i = 1; i < args.length; i++) {
    if (args[i].startsWith('--')) {
      const key = args[i].slice(2).replace(/-/g, '_');
      const value = args[i + 1] && !args[i + 1].startsWith('--') ? args[i + 1] : true;
      options[key] = value;
      if (value !== true) i++;
    }
  }

  const requester = new WalletConnectRequester();
  await requester.init();

  switch (command) {
    case 'connect':
      await requester.connect({
        chains: options.chains?.split(','),
        methods: options.methods?.split(','),
        qr: options.qr
      });
      break;

    case 'request-tx':
      if (!options.to) {
        console.error('Error: --to is required for request-tx');
        process.exit(1);
      }
      await requester.requestTransaction({
        to: options.to,
        data: options.data,
        value: options.value,
        chainId: options.chain || '8453',
        from: options.from
      });
      break;

    case 'request-sign':
      if (!options.message && !options.typed_data) {
        console.error('Error: --message or --typed-data is required for request-sign');
        process.exit(1);
      }
      await requester.requestSignature({
        message: options.message,
        typedData: options.typed_data,
        chainId: options.chain || '8453',
        from: options.from
      });
      break;

    case 'sessions':
      requester.listSessions();
      break;

    case 'disconnect':
      await requester.disconnect(options.topic);
      break;

    default:
      console.error(`Unknown command: ${command}`);
      printHelp();
      process.exit(1);
  }
}

main().catch(console.error);

Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (HIGH)