@portkey/eoa-agent-skills 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # @portkey/eoa-agent-skills
2
2
 
3
+ [中文版](./README.zh-CN.md) | English
4
+
3
5
  AI Agent Skills for Portkey EOA Wallet on [aelf blockchain](https://aelf.com). Provides MCP, CLI, and SDK interfaces for wallet management, token transfers, asset queries, and smart contract interactions.
4
6
 
5
7
  ## Features
@@ -62,7 +64,7 @@ cp .env.example .env
62
64
  | `PORTKEY_NETWORK` | `mainnet` | `mainnet` |
63
65
  | `PORTKEY_API_URL` | Override API base URL | Auto from network |
64
66
  | `PORTKEY_PRIVATE_KEY` | Plaintext private key (optional) | — |
65
- | `PORTKEY_WALLET_DIR` | Custom wallet storage directory | `~/.portkey-agent/wallets/` |
67
+ | `PORTKEY_WALLET_DIR` | Custom wallet storage directory | `~/.portkey/eoa/wallets/` |
66
68
  | `PORTKEY_WALLET_PASSWORD` | Password for local wallet encryption | — |
67
69
 
68
70
  ### MCP Server
@@ -136,14 +138,15 @@ const result = await transfer(config, {
136
138
  console.log('TX:', result.transactionId);
137
139
  ```
138
140
 
139
- ## MCP Tools (20 total)
141
+ ## MCP Tools (21 total)
140
142
 
141
- ### Wallet Management (5)
143
+ ### Wallet Management (6)
142
144
  - `portkey_create_wallet` — Create new wallet with encrypted local storage
143
145
  - `portkey_import_wallet` — Import from mnemonic or private key
144
146
  - `portkey_get_wallet_info` — View wallet public info
145
147
  - `portkey_list_wallets` — List all local wallets
146
148
  - `portkey_backup_wallet` — Export wallet credentials
149
+ - `portkey_delete_wallet` — Delete a local wallet (requires password)
147
150
 
148
151
  ### Asset Queries (7)
149
152
  - `portkey_get_token_list` — Token portfolio with balances
@@ -186,6 +189,10 @@ bun test tests/unit/ # Unit tests
186
189
  bun run tests/e2e/mcp-verify.ts # MCP verification
187
190
  ```
188
191
 
192
+ ## Known Issues
193
+
194
+ - **elliptic <= 6.6.1**: `aelf-sdk` has a transitive dependency on `elliptic` with a known low-severity vulnerability. This is an upstream issue — tracked for resolution when `aelf-sdk` updates its dependency.
195
+
189
196
  ## License
190
197
 
191
198
  MIT
package/README.zh-CN.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # @portkey/eoa-agent-skills
2
2
 
3
+ [English](./README.md) | 中文
4
+
3
5
  [aelf 区块链](https://aelf.com) 上 Portkey EOA 钱包的 AI Agent Skills。提供 MCP、CLI 和 SDK 三种接口,覆盖钱包管理、Token 转账、资产查询和智能合约交互。
4
6
 
5
7
  ## 功能
@@ -62,7 +64,7 @@ cp .env.example .env
62
64
  | `PORTKEY_NETWORK` | `mainnet` | `mainnet` |
63
65
  | `PORTKEY_API_URL` | 覆盖 API 基础 URL | 根据网络自动选择 |
64
66
  | `PORTKEY_PRIVATE_KEY` | 明文私钥(可选) | — |
65
- | `PORTKEY_WALLET_DIR` | 自定义钱包存储目录 | `~/.portkey-agent/wallets/` |
67
+ | `PORTKEY_WALLET_DIR` | 自定义钱包存储目录 | `~/.portkey/eoa/wallets/` |
66
68
  | `PORTKEY_WALLET_PASSWORD` | 本地钱包加密密码 | — |
67
69
 
68
70
  ### MCP Server
@@ -136,14 +138,15 @@ const result = await transfer(config, {
136
138
  console.log('交易ID:', result.transactionId);
137
139
  ```
138
140
 
139
- ## MCP Tools(共 20 个)
141
+ ## MCP Tools(共 21 个)
140
142
 
141
- ### 钱包管理(5
143
+ ### 钱包管理(6
142
144
  - `portkey_create_wallet` — 创建新钱包并加密存储
143
145
  - `portkey_import_wallet` — 导入钱包(助记词/私钥)
144
146
  - `portkey_get_wallet_info` — 查看钱包公开信息
145
147
  - `portkey_list_wallets` — 列出所有本地钱包
146
148
  - `portkey_backup_wallet` — 导出钱包凭证
149
+ - `portkey_delete_wallet` — 删除本地钱包(需密码验证)
147
150
 
148
151
  ### 资产查询(7)
149
152
  - `portkey_get_token_list` — Token 列表和余额
@@ -186,6 +189,10 @@ bun test tests/unit/ # 单元测试
186
189
  bun run tests/e2e/mcp-verify.ts # MCP 验证
187
190
  ```
188
191
 
192
+ ## Known Issues
193
+
194
+ - **elliptic <= 6.6.1**:`aelf-sdk` 的传递依赖 `elliptic` 存在一个 low 级别漏洞,属上游问题,等待 `aelf-sdk` 更新修复。
195
+
189
196
  ## License
190
197
 
191
198
  MIT
package/index.ts CHANGED
@@ -7,7 +7,9 @@ export {
7
7
  getWalletInfo,
8
8
  listWallets,
9
9
  backupWallet,
10
+ deleteWalletByAddress,
10
11
  resolvePrivateKey,
12
+ createSignerFromWallet,
11
13
  } from './src/core/wallet.js';
12
14
 
13
15
  // Core: Queries
@@ -44,12 +46,24 @@ export {
44
46
  // Config
45
47
  export { getConfig } from './lib/config.js';
46
48
 
49
+ // Crypto utilities
50
+ export { generateStrongPassword } from './lib/crypto.js';
51
+
52
+ // --- AelfSigner integration (for use with awaken/eforest DApp skills) ---
53
+ export type { AelfSigner } from '@portkey/aelf-signer';
54
+ export {
55
+ createEoaSigner,
56
+ createSignerFromEnv,
57
+ EoaSigner,
58
+ } from '@portkey/aelf-signer';
59
+
47
60
  // Types
48
61
  export type {
49
62
  PortkeyConfig,
50
63
  NetworkType,
51
64
  ChainInfo,
52
65
  StoredWallet,
66
+ WalletPublicInfo,
53
67
  CreateWalletParams,
54
68
  CreateWalletResult,
55
69
  ImportWalletParams,
package/lib/aelf.ts CHANGED
@@ -32,16 +32,23 @@ interface SendOptions {
32
32
  // Wallet helpers
33
33
  // ============================================================
34
34
 
35
- /** A common read-only private key used for view-only contract calls. */
36
- const COMMON_PRIVATE =
35
+ /**
36
+ * Default read-only private key for view-only contract calls.
37
+ * Override via PORTKEY_READONLY_PRIVATE_KEY env variable.
38
+ */
39
+ const DEFAULT_READONLY_PRIVATE_KEY =
37
40
  'f6e512a3c259e5f9af981d7f99d245aa5bc52fe448495e0b0dd56e8406be6f71';
38
41
 
42
+ function getReadonlyPrivateKey(): string {
43
+ return process.env.PORTKEY_READONLY_PRIVATE_KEY || DEFAULT_READONLY_PRIVATE_KEY;
44
+ }
45
+
39
46
  /**
40
47
  * Create an aelf wallet object from a private key.
41
- * If no key is provided, uses a common read-only key (for view calls).
48
+ * If no key is provided, uses a read-only key (for view calls).
42
49
  */
43
50
  export function getWallet(privateKey?: string): AElfWallet {
44
- return AElf.wallet.getWalletByPrivateKey(privateKey || COMMON_PRIVATE);
51
+ return AElf.wallet.getWalletByPrivateKey(privateKey || getReadonlyPrivateKey());
45
52
  }
46
53
 
47
54
  /**
package/lib/api.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { PortkeyConfig, AddressInfo } from './types.js';
2
+ import { getAelfAddress, getChainIdFromAddress } from './aelf.js';
2
3
 
3
4
  const DEFAULT_TIMEOUT = 30000;
4
5
 
@@ -62,11 +63,15 @@ async function request<T>(
62
63
  // ============================================================
63
64
 
64
65
  function buildAddressInfos(
65
- address: string,
66
+ rawAddress: string,
66
67
  chainId?: string,
67
68
  ): AddressInfo[] {
68
- if (chainId) {
69
- return [{ chainId, address }];
69
+ // Strip ELF_xxx_ChainId format → raw address, and extract chainId if embedded
70
+ const address = getAelfAddress(rawAddress);
71
+ const resolvedChainId = chainId || getChainIdFromAddress(rawAddress);
72
+
73
+ if (resolvedChainId) {
74
+ return [{ chainId: resolvedChainId, address }];
70
75
  }
71
76
  // If no specific chainId, include common aelf chains
72
77
  return [
package/lib/crypto.ts CHANGED
@@ -1,4 +1,22 @@
1
1
  import AElf from 'aelf-sdk';
2
+ import { randomBytes } from 'crypto';
3
+
4
+ const PASSWORD_LENGTH = 24;
5
+ const PASSWORD_CHARSET =
6
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*_+-=';
7
+
8
+ /**
9
+ * Generate a cryptographically strong random password.
10
+ * 24 chars from a 74-char alphabet ≈ 148 bits of entropy.
11
+ */
12
+ export function generateStrongPassword(): string {
13
+ const bytes = randomBytes(PASSWORD_LENGTH);
14
+ let password = '';
15
+ for (let i = 0; i < PASSWORD_LENGTH; i++) {
16
+ password += PASSWORD_CHARSET[bytes[i] % PASSWORD_CHARSET.length];
17
+ }
18
+ return password;
19
+ }
2
20
 
3
21
  /**
4
22
  * AES encrypt a string using aelf-sdk's built-in AES.
package/lib/storage.ts CHANGED
@@ -3,47 +3,123 @@ import * as path from 'path';
3
3
  import * as os from 'os';
4
4
  import type { StoredWallet } from './types.js';
5
5
 
6
- const DEFAULT_WALLET_DIR = path.join(os.homedir(), '.portkey-agent', 'wallets');
6
+ const DEFAULT_WALLET_DIR = path.join(os.homedir(), '.portkey', 'eoa', 'wallets');
7
+
8
+ // Legacy directories from previous versions (newest first)
9
+ const LEGACY_WALLET_DIRS = [
10
+ path.join(os.homedir(), '.portkey-eoa', 'wallets'),
11
+ path.join(os.homedir(), '.portkey-agent', 'wallets'),
12
+ ];
13
+
14
+ // aelf addresses are Base58-encoded (1-9, A-Z, a-z excluding 0, O, I, l)
15
+ const AELF_ADDRESS_RE = /^[1-9A-HJ-NP-Za-km-z]{30,60}$/;
16
+
17
+ let migrationChecked = false;
18
+
19
+ /** Reset migration flag (for testing only). */
20
+ export function _resetMigrationFlag(): void {
21
+ migrationChecked = false;
22
+ }
7
23
 
8
24
  /**
9
25
  * Get the wallet storage directory.
10
- * Priority: PORTKEY_WALLET_DIR env > default ~/.portkey-agent/wallets/
26
+ * Priority: PORTKEY_WALLET_DIR env > default ~/.portkey/eoa/wallets/
11
27
  */
12
28
  export function getWalletDir(): string {
13
29
  return process.env.PORTKEY_WALLET_DIR || DEFAULT_WALLET_DIR;
14
30
  }
15
31
 
16
32
  /**
17
- * Ensure the wallet directory exists.
33
+ * Validate an address string and return a safe absolute file path.
34
+ * Prevents path traversal attacks by:
35
+ * 1. Checking the address matches the Base58 character set
36
+ * 2. Verifying the resolved path stays inside the wallet directory
18
37
  */
19
- function ensureDir(): void {
20
- const dir = getWalletDir();
21
- if (!fs.existsSync(dir)) {
22
- fs.mkdirSync(dir, { recursive: true });
38
+ function safeWalletPath(address: string): string {
39
+ if (!address || !AELF_ADDRESS_RE.test(address)) {
40
+ throw new Error(`Invalid address format: ${address}`);
23
41
  }
42
+ const dir = path.resolve(getWalletDir());
43
+ const resolved = path.resolve(dir, `${address}.json`);
44
+ if (!resolved.startsWith(dir + path.sep) && resolved !== dir) {
45
+ throw new Error('Path traversal detected');
46
+ }
47
+ return resolved;
24
48
  }
25
49
 
26
50
  /**
27
- * Get the file path for a wallet by address.
51
+ * Auto-migrate wallet files from legacy directories to the current directory.
52
+ * Only runs once per process. Copies (does not delete) files from old locations.
53
+ * Skips files that already exist in the new directory (no overwrite).
28
54
  */
29
- function walletPath(address: string): string {
30
- return path.join(getWalletDir(), `${address}.json`);
55
+ function migrateFromLegacy(): void {
56
+ if (migrationChecked) return;
57
+ migrationChecked = true;
58
+
59
+ // Skip migration if user overrode the directory via env
60
+ if (process.env.PORTKEY_WALLET_DIR) return;
61
+
62
+ const targetDir = getWalletDir();
63
+
64
+ for (const legacyDir of LEGACY_WALLET_DIRS) {
65
+ if (!fs.existsSync(legacyDir)) continue;
66
+
67
+ const files = fs.readdirSync(legacyDir).filter((f) => f.endsWith('.json'));
68
+ if (files.length === 0) continue;
69
+
70
+ // Ensure target exists before copying
71
+ if (!fs.existsSync(targetDir)) {
72
+ fs.mkdirSync(targetDir, { recursive: true, mode: 0o700 });
73
+ }
74
+
75
+ let migrated = 0;
76
+ for (const file of files) {
77
+ const dest = path.join(targetDir, file);
78
+ if (fs.existsSync(dest)) continue; // don't overwrite
79
+ const src = path.join(legacyDir, file);
80
+ fs.copyFileSync(src, dest);
81
+ fs.chmodSync(dest, 0o600);
82
+ migrated++;
83
+ }
84
+
85
+ if (migrated > 0) {
86
+ console.error(
87
+ `[portkey] Migrated ${migrated} wallet(s) from ${legacyDir} → ${targetDir}`,
88
+ );
89
+ }
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Ensure the wallet directory exists with restricted permissions (0700).
95
+ * On first call, also checks for legacy directories and auto-migrates.
96
+ */
97
+ function ensureDir(): void {
98
+ const dir = getWalletDir();
99
+ if (!fs.existsSync(dir)) {
100
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
101
+ }
102
+ migrateFromLegacy();
31
103
  }
32
104
 
33
105
  /**
34
- * Save a wallet to local storage (encrypted JSON file).
106
+ * Save a wallet to local storage (encrypted JSON file, mode 0600).
35
107
  */
36
108
  export function saveWallet(wallet: StoredWallet): void {
37
109
  ensureDir();
38
- const filePath = walletPath(wallet.address);
39
- fs.writeFileSync(filePath, JSON.stringify(wallet, null, 2) + '\n', 'utf-8');
110
+ const filePath = safeWalletPath(wallet.address);
111
+ fs.writeFileSync(filePath, JSON.stringify(wallet, null, 2) + '\n', {
112
+ encoding: 'utf-8',
113
+ mode: 0o600,
114
+ });
40
115
  }
41
116
 
42
117
  /**
43
118
  * Load a wallet from local storage by address.
44
119
  */
45
120
  export function loadWallet(address: string): StoredWallet {
46
- const filePath = walletPath(address);
121
+ ensureDir();
122
+ const filePath = safeWalletPath(address);
47
123
  if (!fs.existsSync(filePath)) {
48
124
  throw new Error(`Wallet not found: ${address}`);
49
125
  }
@@ -52,7 +128,9 @@ export function loadWallet(address: string): StoredWallet {
52
128
  }
53
129
 
54
130
  /**
55
- * List all locally stored wallets (public info only).
131
+ * List all locally stored wallets.
132
+ * Returns full StoredWallet objects (internal use).
133
+ * Use core/wallet.ts listWallets() for public-facing sanitized output.
56
134
  */
57
135
  export function listWallets(): StoredWallet[] {
58
136
  ensureDir();
@@ -68,7 +146,8 @@ export function listWallets(): StoredWallet[] {
68
146
  * Delete a wallet file by address.
69
147
  */
70
148
  export function deleteWallet(address: string): void {
71
- const filePath = walletPath(address);
149
+ ensureDir();
150
+ const filePath = safeWalletPath(address);
72
151
  if (fs.existsSync(filePath)) {
73
152
  fs.unlinkSync(filePath);
74
153
  }
@@ -78,5 +157,6 @@ export function deleteWallet(address: string): void {
78
157
  * Check if a wallet exists locally.
79
158
  */
80
159
  export function walletExists(address: string): boolean {
81
- return fs.existsSync(walletPath(address));
160
+ ensureDir();
161
+ return fs.existsSync(safeWalletPath(address));
82
162
  }
package/lib/types.ts CHANGED
@@ -38,18 +38,34 @@ export interface StoredWallet {
38
38
  network: string;
39
39
  }
40
40
 
41
+ /**
42
+ * Public-facing wallet info (no secrets).
43
+ * Used by listWallets and getWalletInfo to avoid leaking encrypted credentials.
44
+ */
45
+ export interface WalletPublicInfo {
46
+ address: string;
47
+ publicKey: { x: string; y: string };
48
+ name: string;
49
+ network: string;
50
+ createdAt: string;
51
+ }
52
+
41
53
  export interface CreateWalletParams {
42
54
  name?: string;
43
- password: string;
55
+ password?: string; // optional — auto-generated if omitted
56
+ redactMnemonic?: boolean; // if true, mnemonic is NOT returned (already encrypted in wallet file, recoverable via wallet backup)
44
57
  }
45
58
 
46
59
  export interface CreateWalletResult {
47
60
  address: string;
48
- mnemonic: string;
61
+ mnemonic?: string; // omitted when redactMnemonic is true
62
+ mnemonicHint?: string; // recovery guidance when mnemonic is redacted
63
+ passwordGenerated?: boolean; // true if password was auto-generated
64
+ password?: string; // included only when auto-generated
49
65
  }
50
66
 
51
67
  export interface ImportWalletParams {
52
- password: string;
68
+ password?: string; // optional — auto-generated if omitted
53
69
  mnemonic?: string;
54
70
  privateKey?: string;
55
71
  name?: string;
@@ -57,6 +73,8 @@ export interface ImportWalletParams {
57
73
 
58
74
  export interface ImportWalletResult {
59
75
  address: string;
76
+ passwordGenerated?: boolean; // true if password was auto-generated
77
+ password?: string; // included only when auto-generated
60
78
  }
61
79
 
62
80
  export interface GetWalletInfoParams {
package/openclaw.json CHANGED
@@ -1,17 +1,52 @@
1
1
  {
2
2
  "name": "portkey-eoa-agent-skills",
3
- "description": "AI Agent Skills for Portkey EOA Wallet on aelf blockchain",
3
+ "description": "AI Agent Skills for Portkey EOA Wallet on aelf blockchain. SECURITY: Never output mnemonic phrases or private keys in channel messages. Auto-generated passwords SHOULD be returned to the user (they need it to unlock the wallet later). Use --redact-mnemonic for wallet creation. Mnemonic is encrypted in the wallet file and recoverable via wallet backup command with the correct password.",
4
4
  "tools": [
5
5
  {
6
6
  "name": "portkey-eoa-wallet-create",
7
- "description": "Create a new EOA wallet",
7
+ "description": "Create a new EOA wallet. Password is auto-generated if omitted. Mnemonic is redacted by default (encrypted in wallet file, recoverable via portkey-eoa-wallet-backup). IMPORTANT: Do NOT output the mnemonic in the channel. The auto-generated password MUST be shown to the user so they can save it — without it the wallet cannot be unlocked.",
8
8
  "command": "bun",
9
- "args": ["run", "portkey_eoa_skill.ts", "wallet", "create"],
9
+ "args": ["run", "portkey_eoa_skill.ts", "wallet", "create", "--redact-mnemonic"],
10
+ "cwd": "."
11
+ },
12
+ {
13
+ "name": "portkey-eoa-wallet-import",
14
+ "description": "Import a wallet from a private key or mnemonic. Password is auto-generated if omitted. IMPORTANT: Do NOT echo the private key or mnemonic back in the channel. The auto-generated password MUST be shown to the user.",
15
+ "command": "bun",
16
+ "args": ["run", "portkey_eoa_skill.ts", "wallet", "import"],
17
+ "cwd": "."
18
+ },
19
+ {
20
+ "name": "portkey-eoa-wallet-list",
21
+ "description": "List all locally stored wallets (public info only: address, name, network)",
22
+ "command": "bun",
23
+ "args": ["run", "portkey_eoa_skill.ts", "wallet", "list"],
24
+ "cwd": "."
25
+ },
26
+ {
27
+ "name": "portkey-eoa-wallet-info",
28
+ "description": "Get public info for a specific wallet",
29
+ "command": "bun",
30
+ "args": ["run", "portkey_eoa_skill.ts", "wallet", "info"],
31
+ "cwd": "."
32
+ },
33
+ {
34
+ "name": "portkey-eoa-wallet-backup",
35
+ "description": "Export mnemonic and private key (requires password). IMPORTANT: NEVER output mnemonic or private key in a channel message. Instruct the user to run this command on their local machine instead.",
36
+ "command": "bun",
37
+ "args": ["run", "portkey_eoa_skill.ts", "wallet", "backup"],
38
+ "cwd": "."
39
+ },
40
+ {
41
+ "name": "portkey-eoa-wallet-delete",
42
+ "description": "Delete a locally stored wallet (requires password verification)",
43
+ "command": "bun",
44
+ "args": ["run", "portkey_eoa_skill.ts", "wallet", "delete"],
10
45
  "cwd": "."
11
46
  },
12
47
  {
13
48
  "name": "portkey-eoa-query-tokens",
14
- "description": "Get token list with balances",
49
+ "description": "Get token list with balances for a wallet address",
15
50
  "command": "bun",
16
51
  "args": ["run", "portkey_eoa_skill.ts", "query", "tokens"],
17
52
  "cwd": "."
@@ -23,26 +58,96 @@
23
58
  "args": ["run", "portkey_eoa_skill.ts", "query", "balance"],
24
59
  "cwd": "."
25
60
  },
61
+ {
62
+ "name": "portkey-eoa-query-price",
63
+ "description": "Get token prices in USD",
64
+ "command": "bun",
65
+ "args": ["run", "portkey_eoa_skill.ts", "query", "price"],
66
+ "cwd": "."
67
+ },
68
+ {
69
+ "name": "portkey-eoa-query-nft-collections",
70
+ "description": "Get NFT collections for a wallet",
71
+ "command": "bun",
72
+ "args": ["run", "portkey_eoa_skill.ts", "query", "nft-collections"],
73
+ "cwd": "."
74
+ },
75
+ {
76
+ "name": "portkey-eoa-query-nft-items",
77
+ "description": "Get NFT items in a collection",
78
+ "command": "bun",
79
+ "args": ["run", "portkey_eoa_skill.ts", "query", "nft-items"],
80
+ "cwd": "."
81
+ },
26
82
  {
27
83
  "name": "portkey-eoa-query-history",
28
- "description": "Get transaction history",
84
+ "description": "Get transaction history for a wallet",
29
85
  "command": "bun",
30
86
  "args": ["run", "portkey_eoa_skill.ts", "query", "history"],
31
87
  "cwd": "."
32
88
  },
89
+ {
90
+ "name": "portkey-eoa-query-tx",
91
+ "description": "Get transaction detail by ID",
92
+ "command": "bun",
93
+ "args": ["run", "portkey_eoa_skill.ts", "query", "tx"],
94
+ "cwd": "."
95
+ },
33
96
  {
34
97
  "name": "portkey-eoa-transfer",
35
- "description": "Send a token transfer",
98
+ "description": "Send a same-chain token transfer",
36
99
  "command": "bun",
37
100
  "args": ["run", "portkey_eoa_skill.ts", "transfer"],
38
101
  "cwd": "."
39
102
  },
103
+ {
104
+ "name": "portkey-eoa-cross-chain-transfer",
105
+ "description": "Send a cross-chain token transfer between aelf chains",
106
+ "command": "bun",
107
+ "args": ["run", "portkey_eoa_skill.ts", "transfer", "--cross-chain"],
108
+ "cwd": "."
109
+ },
40
110
  {
41
111
  "name": "portkey-eoa-contract-view",
42
112
  "description": "Call a read-only contract method",
43
113
  "command": "bun",
44
114
  "args": ["run", "portkey_eoa_skill.ts", "contract", "view"],
45
115
  "cwd": "."
116
+ },
117
+ {
118
+ "name": "portkey-eoa-contract-send",
119
+ "description": "Call a state-changing contract method (requires wallet)",
120
+ "command": "bun",
121
+ "args": ["run", "portkey_eoa_skill.ts", "contract", "send"],
122
+ "cwd": "."
123
+ },
124
+ {
125
+ "name": "portkey-eoa-contract-approve",
126
+ "description": "Approve token spending allowance",
127
+ "command": "bun",
128
+ "args": ["run", "portkey_eoa_skill.ts", "contract", "approve"],
129
+ "cwd": "."
130
+ },
131
+ {
132
+ "name": "portkey-eoa-contract-estimate-fee",
133
+ "description": "Estimate transaction fee before sending",
134
+ "command": "bun",
135
+ "args": ["run", "portkey_eoa_skill.ts", "contract", "estimate-fee"],
136
+ "cwd": "."
137
+ },
138
+ {
139
+ "name": "portkey-eoa-ebridge-transfer",
140
+ "description": "Cross-chain transfer via eBridge (aelf <-> EVM)",
141
+ "command": "bun",
142
+ "args": ["run", "portkey_eoa_skill.ts", "ebridge", "transfer"],
143
+ "cwd": "."
144
+ },
145
+ {
146
+ "name": "portkey-eoa-ebridge-limit",
147
+ "description": "Query eBridge transfer limits",
148
+ "command": "bun",
149
+ "args": ["run", "portkey_eoa_skill.ts", "ebridge", "limit"],
150
+ "cwd": "."
46
151
  }
47
152
  ]
48
153
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@portkey/eoa-agent-skills",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "AI Agent Skills for Portkey EOA Wallet — MCP, CLI, and SDK interfaces for wallet management, token transfers, asset queries, and contract interactions on aelf blockchain.",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -39,8 +39,9 @@
39
39
  "test:e2e": "bun test tests/e2e/"
40
40
  },
41
41
  "dependencies": {
42
- "aelf-sdk": "^3.5.1-beta.0",
43
42
  "@modelcontextprotocol/sdk": "^1.26.0",
43
+ "@portkey/aelf-signer": "^1.0.0",
44
+ "aelf-sdk": "^3.5.1-beta.0",
44
45
  "commander": "^12.1.0",
45
46
  "zod": "^3.24.0"
46
47
  },
@@ -9,6 +9,7 @@ import {
9
9
  getWalletInfo,
10
10
  listWallets,
11
11
  backupWallet,
12
+ deleteWalletByAddress,
12
13
  } from './src/core/wallet.js';
13
14
  import {
14
15
  getChainInfo,
@@ -45,14 +46,16 @@ const wallet = program.command('wallet').description('Wallet management');
45
46
  wallet
46
47
  .command('create')
47
48
  .description('Create a new wallet')
48
- .requiredOption('--password <password>', 'Encryption password')
49
+ .option('--password <password>', 'Encryption password (auto-generated if omitted)')
49
50
  .option('--name <name>', 'Wallet name')
51
+ .option('--redact-mnemonic', 'Save mnemonic to local file instead of printing')
50
52
  .action(async (opts) => {
51
53
  try {
52
54
  const config = getConfig(program.opts().network);
53
55
  const result = await createWallet(config, {
54
56
  password: opts.password,
55
57
  name: opts.name,
58
+ redactMnemonic: opts.redactMnemonic,
56
59
  });
57
60
  outputSuccess(result);
58
61
  } catch (err: any) {
@@ -63,7 +66,7 @@ wallet
63
66
  wallet
64
67
  .command('import')
65
68
  .description('Import a wallet from mnemonic or private key')
66
- .requiredOption('--password <password>', 'Encryption password')
69
+ .option('--password <password>', 'Encryption password (auto-generated if omitted)')
67
70
  .option('--mnemonic <mnemonic>', '12-word mnemonic phrase')
68
71
  .option('--private-key <key>', '64-char hex private key')
69
72
  .option('--name <name>', 'Wallet name')
@@ -127,6 +130,24 @@ wallet
127
130
  }
128
131
  });
129
132
 
133
+ wallet
134
+ .command('delete')
135
+ .description('Delete a locally stored wallet (requires password verification)')
136
+ .requiredOption('--address <address>', 'Wallet address to delete')
137
+ .requiredOption('--password <password>', 'Wallet password (for verification)')
138
+ .action(async (opts) => {
139
+ try {
140
+ const config = getConfig(program.opts().network);
141
+ const result = await deleteWalletByAddress(config, {
142
+ address: opts.address,
143
+ password: opts.password,
144
+ });
145
+ outputSuccess(result);
146
+ } catch (err: any) {
147
+ outputError(err.message);
148
+ }
149
+ });
150
+
130
151
  // ============================================================
131
152
  // Query commands
132
153
  // ============================================================
@@ -45,6 +45,12 @@ export async function eBridgeTransfer(
45
45
  spender: params.bridgeContractAddress,
46
46
  });
47
47
 
48
+ if (allowanceResult.error) {
49
+ throw new Error(
50
+ `GetAllowance failed: ${allowanceResult.error.message || 'Unknown error'}`,
51
+ );
52
+ }
53
+
48
54
  const currentAllowance = BigInt(
49
55
  allowanceResult.data?.allowance || allowanceResult.data?.Allowance || '0',
50
56
  );
@@ -152,17 +152,30 @@ export async function estimateTransactionFee(
152
152
  );
153
153
  }
154
154
 
155
- // Calculate fee via RPC
156
- const res = await fetch(
157
- `${rpcUrl}/api/blockChain/calculateTransactionFee`,
158
- {
159
- method: 'POST',
160
- headers: { 'Content-Type': 'application/json' },
161
- body: JSON.stringify({ RawTransaction: rawResult.data }),
162
- },
163
- );
164
-
165
- const txFee = await res.json();
155
+ // Calculate fee via RPC (with timeout and status check)
156
+ const controller = new AbortController();
157
+ const timer = setTimeout(() => controller.abort(), 30000);
158
+ let txFee: any;
159
+ try {
160
+ const res = await fetch(
161
+ `${rpcUrl}/api/blockChain/calculateTransactionFee`,
162
+ {
163
+ method: 'POST',
164
+ headers: { 'Content-Type': 'application/json' },
165
+ body: JSON.stringify({ RawTransaction: rawResult.data }),
166
+ signal: controller.signal,
167
+ },
168
+ );
169
+ if (!res.ok) {
170
+ const text = await res.text().catch(() => '');
171
+ throw new Error(
172
+ `RPC error ${res.status}: ${text || res.statusText}`,
173
+ );
174
+ }
175
+ txFee = await res.json();
176
+ } finally {
177
+ clearTimeout(timer);
178
+ }
166
179
  if (!txFee?.Success) {
167
180
  throw new Error('Failed to calculate transaction fee');
168
181
  }
@@ -9,13 +9,15 @@ import type {
9
9
  BackupWalletParams,
10
10
  BackupWalletResult,
11
11
  StoredWallet,
12
+ WalletPublicInfo,
12
13
  } from '../../lib/types.js';
13
- import { encrypt, decrypt } from '../../lib/crypto.js';
14
+ import { encrypt, decrypt, generateStrongPassword } from '../../lib/crypto.js';
14
15
  import {
15
16
  saveWallet,
16
17
  loadWallet,
17
18
  listWallets as listStoredWallets,
18
19
  walletExists,
20
+ deleteWallet as deleteStoredWallet,
19
21
  } from '../../lib/storage.js';
20
22
  import {
21
23
  createNewWallet,
@@ -31,8 +33,9 @@ export async function createWallet(
31
33
  config: PortkeyConfig,
32
34
  params: CreateWalletParams,
33
35
  ): Promise<CreateWalletResult> {
34
- const { password, name } = params;
35
- if (!password) throw new Error('password is required');
36
+ const { name, redactMnemonic } = params;
37
+ const passwordGenerated = !params.password;
38
+ const password = params.password || generateStrongPassword();
36
39
 
37
40
  const walletInfo = createNewWallet();
38
41
 
@@ -50,10 +53,30 @@ export async function createWallet(
50
53
 
51
54
  saveWallet(stored);
52
55
 
53
- return {
56
+ const result: CreateWalletResult = {
54
57
  address: walletInfo.address,
55
- mnemonic: walletInfo.mnemonic || '',
56
58
  };
59
+
60
+ // Mnemonic handling: redact or return
61
+ // When redacted, mnemonic is NOT returned — it's already AES-encrypted in the wallet file
62
+ // and can be recovered via `wallet backup --address <addr> --password <pwd>`
63
+ if (redactMnemonic) {
64
+ result.mnemonicHint =
65
+ 'Mnemonic is encrypted and stored in your wallet file. ' +
66
+ 'To recover it, run: wallet backup --address ' +
67
+ walletInfo.address +
68
+ ' --password <your_password>';
69
+ } else {
70
+ result.mnemonic = walletInfo.mnemonic || '';
71
+ }
72
+
73
+ // Password: include in result only when auto-generated
74
+ if (passwordGenerated) {
75
+ result.passwordGenerated = true;
76
+ result.password = password;
77
+ }
78
+
79
+ return result;
57
80
  }
58
81
 
59
82
  // ============================================================
@@ -64,8 +87,10 @@ export async function importWallet(
64
87
  config: PortkeyConfig,
65
88
  params: ImportWalletParams,
66
89
  ): Promise<ImportWalletResult> {
67
- const { password, mnemonic, privateKey, name } = params;
68
- if (!password) throw new Error('password is required');
90
+ const { mnemonic, privateKey, name } = params;
91
+ const passwordGenerated = !params.password;
92
+ const password = params.password || generateStrongPassword();
93
+
69
94
  if (!mnemonic && !privateKey) {
70
95
  throw new Error('Either mnemonic or privateKey is required');
71
96
  }
@@ -101,7 +126,12 @@ export async function importWallet(
101
126
 
102
127
  saveWallet(stored);
103
128
 
104
- return { address: walletInfo.address };
129
+ const result: ImportWalletResult = { address: walletInfo.address };
130
+ if (passwordGenerated) {
131
+ result.passwordGenerated = true;
132
+ result.password = password;
133
+ }
134
+ return result;
105
135
  }
106
136
 
107
137
  // ============================================================
@@ -123,13 +153,38 @@ export async function getWalletInfo(
123
153
  }
124
154
 
125
155
  // ============================================================
126
- // listWallets — List all local wallets (public info only)
156
+ // listWallets — List all local wallets (public info only, sanitized)
127
157
  // ============================================================
128
158
 
129
159
  export async function listWallets(
130
160
  _config: PortkeyConfig,
131
- ): Promise<StoredWallet[]> {
132
- return listStoredWallets();
161
+ ): Promise<WalletPublicInfo[]> {
162
+ return listStoredWallets().map((w) => ({
163
+ address: w.address,
164
+ publicKey: w.publicKey,
165
+ name: w.name,
166
+ network: w.network,
167
+ createdAt: w.createdAt,
168
+ }));
169
+ }
170
+
171
+ // ============================================================
172
+ // deleteWallet — Remove a locally stored wallet
173
+ // ============================================================
174
+
175
+ export async function deleteWalletByAddress(
176
+ _config: PortkeyConfig,
177
+ params: { address: string; password: string },
178
+ ): Promise<{ address: string; deleted: boolean }> {
179
+ const { address, password } = params;
180
+ if (!password) throw new Error('password is required');
181
+
182
+ // Verify password is correct before deleting
183
+ const stored = loadWallet(address);
184
+ decrypt(stored.AESEncryptPrivateKey, password);
185
+
186
+ deleteStoredWallet(address);
187
+ return { address, deleted: true };
133
188
  }
134
189
 
135
190
  // ============================================================
@@ -198,3 +253,28 @@ function extractPublicKey(walletInfo: any): { x: string; y: string } {
198
253
  return { x: '', y: '' };
199
254
  }
200
255
  }
256
+
257
+ // ============================================================
258
+ // AelfSigner bridge — create signer from resolved wallet
259
+ // ============================================================
260
+
261
+ import { createEoaSigner, type AelfSigner } from '@portkey/aelf-signer';
262
+
263
+ /**
264
+ * Create an AelfSigner (EoaSigner) from the resolved private key.
265
+ * Uses the same resolution logic as eoa-agent-skills:
266
+ * 1. Direct privateKey param
267
+ * 2. PORTKEY_PRIVATE_KEY env var
268
+ * 3. Local wallet file (address + password)
269
+ *
270
+ * The returned signer can be used with awaken-agent-skills / eforest-agent-skills.
271
+ */
272
+ export function createSignerFromWallet(params: {
273
+ privateKey?: string;
274
+ address?: string;
275
+ password?: string;
276
+ }): AelfSigner {
277
+ const pk = resolvePrivateKey(params);
278
+ return createEoaSigner(pk);
279
+ }
280
+
package/src/mcp/server.ts CHANGED
@@ -11,6 +11,7 @@ import {
11
11
  getWalletInfo,
12
12
  listWallets,
13
13
  backupWallet,
14
+ deleteWalletByAddress,
14
15
  } from '../core/wallet.js';
15
16
  import {
16
17
  getChainInfo,
@@ -65,29 +66,41 @@ function fail(err: any) {
65
66
  }
66
67
 
67
68
  // ============================================================
68
- // Wallet Management Tools (5)
69
+ // Wallet Management Tools (6)
69
70
  // ============================================================
70
71
 
71
72
  server.registerTool(
72
73
  'portkey_create_wallet',
73
74
  {
74
75
  description:
75
- 'Create a new EOA wallet on aelf blockchain. Generates a mnemonic and private key, encrypts them with the provided password, and stores the wallet locally. Use when a user needs a fresh wallet. Returns address and mnemonic (save the mnemonic — it cannot be recovered).',
76
+ 'Create a new EOA wallet on aelf blockchain. Generates a mnemonic and private key, encrypts them with a password, and stores the wallet locally.\n\nPassword behavior: If password is omitted, a strong password is auto-generated and returned in the response. After creation, ask the user if they want to persist the password by setting PORTKEY_WALLET_PASSWORD in their environment config. If they decline, remind them to save the password securely — it cannot be recovered.\n\nMnemonic safety: For channel-based environments (Slack, Discord, OpenClaw channels), set redactMnemonic=true so the mnemonic is NOT returned in the response. The mnemonic is already AES-encrypted in the wallet file and can be recovered later via wallet backup with the correct password. For local AI tools (Claude Desktop, Cursor), returning the mnemonic directly is acceptable.',
76
77
  inputSchema: {
77
78
  name: z.string().optional().describe('Human-readable wallet name'),
78
79
  password: z
79
80
  .string()
80
- .describe('Password to encrypt the wallet private key and mnemonic'),
81
+ .optional()
82
+ .describe(
83
+ 'Password to encrypt the wallet. If omitted, a strong password is auto-generated.',
84
+ ),
85
+ redactMnemonic: z
86
+ .boolean()
87
+ .optional()
88
+ .default(false)
89
+ .describe(
90
+ 'If true, mnemonic is NOT returned in the response (it is already AES-encrypted in the wallet file, recoverable via wallet backup). Use for channel-based environments (Slack, Discord, OpenClaw) to prevent mnemonic leakage.',
91
+ ),
81
92
  network: z
82
93
  .enum(['mainnet'])
83
94
  .default('mainnet')
84
95
  .describe('Network (mainnet)'),
85
96
  },
86
97
  },
87
- async ({ name, password, network }) => {
98
+ async ({ name, password, redactMnemonic, network }) => {
88
99
  try {
89
100
  const config = getConfig(network);
90
- return ok(await createWallet(config, { name, password }));
101
+ return ok(
102
+ await createWallet(config, { name, password, redactMnemonic }),
103
+ );
91
104
  } catch (err) {
92
105
  return fail(err);
93
106
  }
@@ -98,7 +111,7 @@ server.registerTool(
98
111
  'portkey_import_wallet',
99
112
  {
100
113
  description:
101
- 'Import an existing wallet using a 12-word mnemonic phrase or a 64-character hex private key. Encrypts and stores the wallet locally. Use when a user wants to use an existing wallet.',
114
+ 'Import an existing wallet using a 12-word mnemonic phrase or a 64-character hex private key. Encrypts and stores the wallet locally.\n\nPassword behavior: If password is omitted, a strong password is auto-generated and returned. After import, ask the user if they want to persist the password by setting PORTKEY_WALLET_PASSWORD in their environment. If they decline, remind them to save it securely.\n\nSecurity: Never echo the user\'s mnemonic or private key back in channel-based conversations (Slack, Discord, OpenClaw).',
102
115
  inputSchema: {
103
116
  mnemonic: z
104
117
  .string()
@@ -110,7 +123,10 @@ server.registerTool(
110
123
  .describe('64-character hex private key (with or without 0x prefix)'),
111
124
  password: z
112
125
  .string()
113
- .describe('Password to encrypt the wallet'),
126
+ .optional()
127
+ .describe(
128
+ 'Password to encrypt the wallet. If omitted, a strong password is auto-generated.',
129
+ ),
114
130
  name: z.string().optional().describe('Human-readable wallet name'),
115
131
  network: z
116
132
  .enum(['mainnet'])
@@ -199,6 +215,34 @@ server.registerTool(
199
215
  },
200
216
  );
201
217
 
218
+ server.registerTool(
219
+ 'portkey_delete_wallet',
220
+ {
221
+ description:
222
+ 'Delete a locally stored wallet file. Requires the wallet password for verification — the password must be correct before the wallet is removed. Use when a user explicitly wants to remove a wallet from local storage.',
223
+ inputSchema: {
224
+ address: z.string().describe('Wallet address to delete'),
225
+ password: z
226
+ .string()
227
+ .describe('Wallet password (must be correct to authorize deletion)'),
228
+ network: z
229
+ .enum(['mainnet'])
230
+ .default('mainnet')
231
+ .describe('Network (mainnet)'),
232
+ },
233
+ },
234
+ async ({ address, password, network }) => {
235
+ try {
236
+ const config = getConfig(network);
237
+ return ok(
238
+ await deleteWalletByAddress(config, { address, password }),
239
+ );
240
+ } catch (err) {
241
+ return fail(err);
242
+ }
243
+ },
244
+ );
245
+
202
246
  // ============================================================
203
247
  // Asset Query Tools (7)
204
248
  // ============================================================