@eforest-finance/agent-skills 0.2.0 → 0.3.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/.env.example CHANGED
@@ -1,8 +1,16 @@
1
+ # ---- Wallet Configuration (pick ONE mode) ----
2
+
3
+ # Mode 1: EOA (direct private key signing)
1
4
  # aelf wallet private key (hex string, 64 chars)
2
- # Used for signing transactions on aelf blockchain.
3
5
  # WARNING: Never commit real private keys to version control.
4
6
  AELF_PRIVATE_KEY=your_private_key_here
5
7
 
8
+ # Mode 2: CA (Portkey Contract Account via ManagerForwardCall)
9
+ # PORTKEY_PRIVATE_KEY=your_manager_private_key
10
+ # PORTKEY_CA_HASH=your_ca_hash
11
+ # PORTKEY_CA_ADDRESS=your_ca_address
12
+ # PORTKEY_ORIGIN_CHAIN_ID=AELF
13
+
6
14
  # Optional: override environment (default: mainnet)
7
15
  # AELF_ENV=testnet
8
16
 
package/README.md CHANGED
@@ -13,7 +13,7 @@ AI Agent Kit for aelf token lifecycle on [eForest](https://www.eforest.finance).
13
13
  ### Prerequisites
14
14
 
15
15
  - [Bun](https://bun.sh) >= 1.0
16
- - An aelf wallet private key with ELF balance
16
+ - An aelf wallet private key (EOA) or Portkey CA wallet credentials
17
17
 
18
18
  ### Install
19
19
 
@@ -76,6 +76,8 @@ Then edit the generated config to replace `<YOUR_PRIVATE_KEY>` with your actual
76
76
 
77
77
  If you prefer manual configuration, add this to your MCP settings:
78
78
 
79
+ **EOA mode** (direct private key signing):
80
+
79
81
  ```json
80
82
  {
81
83
  "mcpServers": {
@@ -91,6 +93,25 @@ If you prefer manual configuration, add this to your MCP settings:
91
93
  }
92
94
  ```
93
95
 
96
+ **CA mode** (Portkey Contract Account):
97
+
98
+ ```json
99
+ {
100
+ "mcpServers": {
101
+ "eforest-token": {
102
+ "command": "bun",
103
+ "args": ["run", "/path/to/eforest-agent-skills/src/mcp/server.ts"],
104
+ "env": {
105
+ "PORTKEY_PRIVATE_KEY": "your_manager_private_key",
106
+ "PORTKEY_CA_HASH": "your_ca_hash",
107
+ "PORTKEY_CA_ADDRESS": "your_ca_address",
108
+ "EFOREST_NETWORK": "mainnet"
109
+ }
110
+ }
111
+ }
112
+ }
113
+ ```
114
+
94
115
  ## OpenClaw
95
116
 
96
117
  ```bash
@@ -176,7 +197,10 @@ bun test:integration # Integration tests only
176
197
 
177
198
  | Variable | Description | Default |
178
199
  |----------|-------------|---------|
179
- | `AELF_PRIVATE_KEY` | aelf wallet private key | (required) |
200
+ | `AELF_PRIVATE_KEY` | aelf wallet private key (EOA mode) | — |
201
+ | `PORTKEY_PRIVATE_KEY` | Portkey Manager private key (CA mode) | — |
202
+ | `PORTKEY_CA_HASH` | Portkey CA hash (CA mode) | — |
203
+ | `PORTKEY_CA_ADDRESS` | Portkey CA address (CA mode) | — |
180
204
  | `EFOREST_NETWORK` / `AELF_ENV` | `mainnet` or `testnet` | `mainnet` |
181
205
  | `EFOREST_API_URL` / `AELF_API_URL` | Backend API URL | auto |
182
206
  | `EFOREST_RPC_URL` / `AELF_RPC_URL` | AELF MainChain RPC | auto |
@@ -5,6 +5,7 @@
5
5
  */
6
6
 
7
7
  import AElf from 'aelf-sdk';
8
+ import type { AelfSigner } from '@portkey/aelf-signer';
8
9
  import { TX_POLL_INTERVAL_MS, TX_POLL_MAX_RETRIES } from './types';
9
10
 
10
11
  // ============================================================================
@@ -23,16 +24,24 @@ export function getWallet(privateKey?: string): any {
23
24
  const key =
24
25
  process.env.AELF_PRIVATE_KEY ||
25
26
  process.env.EFOREST_PRIVATE_KEY ||
27
+ process.env.PORTKEY_PRIVATE_KEY ||
26
28
  privateKey;
27
29
 
28
30
  if (!key) {
29
31
  throw new Error(
30
- 'Private key is required. Set AELF_PRIVATE_KEY env var or pass --private-key.',
32
+ 'Private key is required. Set AELF_PRIVATE_KEY (EOA) or PORTKEY_PRIVATE_KEY (CA) env var, or pass --private-key.',
31
33
  );
32
34
  }
33
35
  return AElf.wallet.getWalletByPrivateKey(key);
34
36
  }
35
37
 
38
+ // Singleton view wallet for read-only calls (no real identity needed)
39
+ let _viewWallet: any = null;
40
+ function getViewWallet(): any {
41
+ if (!_viewWallet) _viewWallet = AElf.wallet.createNewWallet();
42
+ return _viewWallet;
43
+ }
44
+
36
45
  // ============================================================================
37
46
  // Contract
38
47
  // ============================================================================
@@ -105,32 +114,36 @@ export async function getTxResult(
105
114
  // Contract Call Helpers
106
115
  // ============================================================================
107
116
 
117
+ /**
118
+ * Send a state-changing contract call via AelfSigner.
119
+ * Supports both EOA (direct signing) and CA (ManagerForwardCall) transparently.
120
+ */
108
121
  export async function callContractSend(
109
122
  rpcUrl: string,
110
123
  contractAddress: string,
111
124
  methodName: string,
112
125
  params: any,
113
- wallet: any,
126
+ signer: AelfSigner,
114
127
  ): Promise<{ TransactionId: string; txResult: any }> {
115
- const contract = await getContractInstance(rpcUrl, contractAddress, wallet);
116
- const tx = await contract[methodName](params);
117
- const transactionId = tx.TransactionId || tx.transactionId || tx;
118
- if (!transactionId || typeof transactionId !== 'string') {
119
- throw new Error(
120
- `Failed to get TransactionId from ${methodName}. Response: ${JSON.stringify(tx)}`,
121
- );
122
- }
123
- await sleep(TX_POLL_INTERVAL_MS);
124
- return getTxResult(rpcUrl, transactionId);
128
+ const result = await signer.sendContractCall(rpcUrl, contractAddress, methodName, params);
129
+ return {
130
+ TransactionId: result.transactionId,
131
+ txResult: result.txResult,
132
+ };
125
133
  }
126
134
 
135
+ /**
136
+ * Read-only contract call. Wallet parameter is optional — uses an internal
137
+ * ephemeral wallet if not provided.
138
+ */
127
139
  export async function callContractView(
128
140
  rpcUrl: string,
129
141
  contractAddress: string,
130
142
  methodName: string,
131
143
  params: any,
132
- wallet: any,
144
+ wallet?: any,
133
145
  ): Promise<any> {
134
- const contract = await getContractInstance(rpcUrl, contractAddress, wallet);
146
+ const w = wallet || getViewWallet();
147
+ const contract = await getContractInstance(rpcUrl, contractAddress, w);
135
148
  return await contract[methodName].call(params);
136
149
  }
package/lib/config.ts CHANGED
@@ -16,6 +16,7 @@ import { existsSync, readFileSync } from 'fs';
16
16
  import { resolve, dirname } from 'path';
17
17
  import { fileURLToPath } from 'url';
18
18
 
19
+ import { createSignerFromEnv } from '@portkey/aelf-signer';
19
20
  import type { CmsConfigItems, ResolvedConfig } from './types';
20
21
  import { ENV_PRESETS } from './types';
21
22
  import { getWallet } from './aelf-client';
@@ -124,8 +125,11 @@ export async function getNetworkConfig(opts?: {
124
125
  '',
125
126
  };
126
127
 
128
+ // Unified signer: detects EOA/CA mode from env vars automatically
129
+ const signer = createSignerFromEnv();
130
+ // Raw wallet for API auth (fetchAuthToken needs keyPair for signature)
127
131
  const wallet = getWallet(o.privateKey);
128
- const walletAddress = wallet.address;
132
+ const walletAddress = signer.address;
129
133
 
130
134
  return {
131
135
  apiUrl,
@@ -133,6 +137,7 @@ export async function getNetworkConfig(opts?: {
133
137
  connectUrl,
134
138
  rpcUrls,
135
139
  contracts: cmsConfig,
140
+ signer,
136
141
  wallet,
137
142
  walletAddress,
138
143
  };
package/lib/types.ts CHANGED
@@ -104,7 +104,11 @@ export interface ResolvedConfig {
104
104
  connectUrl: string;
105
105
  rpcUrls: Record<string, string>;
106
106
  contracts: CmsConfigItems;
107
- wallet: any; // aelf-sdk wallet instance
107
+ /** Unified signer supports both EOA and CA wallets. Use for all contract calls. */
108
+ signer: import('@portkey/aelf-signer').AelfSigner;
109
+ /** Raw wallet for API auth (fetchAuthToken). For CA: manager wallet. */
110
+ wallet: any;
111
+ /** Identity address: CA address (CA mode) or wallet address (EOA mode). */
108
112
  walletAddress: string;
109
113
  }
110
114
 
package/openclaw.json CHANGED
@@ -3,7 +3,7 @@
3
3
  {
4
4
  "name": "aelf-buy-seed",
5
5
  "command": "bun run create_token_skill.ts buy-seed --symbol {{symbol}} --issuer {{issuer}} --env {{env}} {{#if force}}--force {{force}}{{/if}} {{#if privateKey}}--private-key {{privateKey}}{{/if}} {{#if dryRun}}--dry-run{{/if}}",
6
- "description": "Purchase a SEED on the aelf MainChain. A SEED is a prerequisite for creating a token on aelf blockchain. IMPORTANT: By default this tool REFUSES to buy and outputs the SEED price. You MUST first run with --dry-run to check the price, then ASK THE USER if they want to proceed, then re-run with --force to confirm. Workflow: (1) dry-run to see price, (2) show price to user and ask for confirmation, (3) run with --force or --force <maxELF> to execute. --force with no value buys unconditionally. --force 2 buys only if price <= 2 ELF. The full buy flow is: query price -> check ELF balance -> approve ELF -> call SymbolRegister.Buy -> parse SEED symbol from tx logs. IMPORTANT: The output includes 'seedSymbol' (e.g. 'SEED-321') which is the real on-chain SEED identifier. You MUST use this seedSymbol value for the subsequent create-token --seed-symbol parameter, NOT the token symbol name. Requires AELF_PRIVATE_KEY env var or --private-key. Output: JSON with {success, transactionId, seedSymbol, data} on success, or '[ERROR] ...' with price info on refusal.",
6
+ "description": "Purchase a SEED on the aelf MainChain. A SEED is a prerequisite for creating a token on aelf blockchain. IMPORTANT: By default this tool REFUSES to buy and outputs the SEED price. You MUST first run with --dry-run to check the price, then ASK THE USER if they want to proceed, then re-run with --force to confirm. Workflow: (1) dry-run to see price, (2) show price to user and ask for confirmation, (3) run with --force or --force <maxELF> to execute. --force with no value buys unconditionally. --force 2 buys only if price <= 2 ELF. The full buy flow is: query price -> check ELF balance -> approve ELF -> call SymbolRegister.Buy -> parse SEED symbol from tx logs. IMPORTANT: The output includes 'seedSymbol' (e.g. 'SEED-321') which is the real on-chain SEED identifier. You MUST use this seedSymbol value for the subsequent create-token --seed-symbol parameter, NOT the token symbol name. Requires wallet env vars: AELF_PRIVATE_KEY (EOA) or PORTKEY_PRIVATE_KEY + PORTKEY_CA_HASH + PORTKEY_CA_ADDRESS (CA). Output: JSON with {success, transactionId, seedSymbol, data} on success, or '[ERROR] ...' with price info on refusal.",
7
7
  "working_directory": "scripts/skills",
8
8
  "parameters": {
9
9
  "symbol": {
@@ -30,7 +30,7 @@
30
30
  "privateKey": {
31
31
  "type": "string",
32
32
  "required": false,
33
- "description": "aelf wallet private key. Prefer using AELF_PRIVATE_KEY env var instead."
33
+ "description": "aelf wallet private key. Prefer using AELF_PRIVATE_KEY (EOA) or PORTKEY_PRIVATE_KEY (CA) env var instead."
34
34
  },
35
35
  "dryRun": {
36
36
  "type": "boolean",
@@ -43,7 +43,7 @@
43
43
  {
44
44
  "name": "aelf-create-token",
45
45
  "command": "bun run create_token_skill.ts create-token --symbol {{symbol}} --token-name '{{tokenName}}' --seed-symbol {{seedSymbol}} --total-supply {{totalSupply}} --decimals {{decimals}} {{#if issuer}}--issuer {{issuer}}{{/if}} --issue-chain {{issueChain}} {{#if isBurnable}}--is-burnable{{else}}--no-is-burnable{{/if}} {{#if tokenImage}}--token-image '{{tokenImage}}'{{/if}} --env {{env}} {{#if privateKey}}--private-key {{privateKey}}{{/if}} {{#if dryRun}}--dry-run{{/if}}",
46
- "description": "Create a new fungible token (FT) on the aelf blockchain using an owned SEED. The full flow is: (1) Check and approve SEED allowance for the TokenAdapter contract, (2) Call TokenAdapter.CreateToken on MainChain, (3) Query GetTokenInfo for proxyIssuer, (4) Save token metadata to backend, (5) Sync token to the issue chain (with graceful degradation on timeout). IMPORTANT: --seed-symbol must be the SEED-{number} value from buy-seed output (e.g. 'SEED-321'), NOT the token name. --issuer is OPTIONAL and defaults to your wallet address. NOTE: The on-chain issuer will be a proxy account created by TokenAdapter, NOT the address you pass. The output includes 'proxyIssuer' which is needed by issue-token. Cross-chain sync to side chain may take several minutes; if it times out, the output includes a warning but success=true (token was created on MainChain). Requires AELF_PRIVATE_KEY env var or --private-key. Output: JSON with {success, transactionId, proxyIssuer, crossChainSynced, data} on success.",
46
+ "description": "Create a new fungible token (FT) on the aelf blockchain using an owned SEED. The full flow is: (1) Check and approve SEED allowance for the TokenAdapter contract, (2) Call TokenAdapter.CreateToken on MainChain, (3) Query GetTokenInfo for proxyIssuer, (4) Save token metadata to backend, (5) Sync token to the issue chain (with graceful degradation on timeout). IMPORTANT: --seed-symbol must be the SEED-{number} value from buy-seed output (e.g. 'SEED-321'), NOT the token name. --issuer is OPTIONAL and defaults to your wallet address. NOTE: The on-chain issuer will be a proxy account created by TokenAdapter, NOT the address you pass. The output includes 'proxyIssuer' which is needed by issue-token. Cross-chain sync to side chain may take several minutes; if it times out, the output includes a warning but success=true (token was created on MainChain). Requires wallet env vars: AELF_PRIVATE_KEY (EOA) or PORTKEY_PRIVATE_KEY + PORTKEY_CA_HASH + PORTKEY_CA_ADDRESS (CA). Output: JSON with {success, transactionId, proxyIssuer, crossChainSynced, data} on success.",
47
47
  "working_directory": "scripts/skills",
48
48
  "parameters": {
49
49
  "symbol": {
@@ -101,7 +101,7 @@
101
101
  "privateKey": {
102
102
  "type": "string",
103
103
  "required": false,
104
- "description": "aelf wallet private key. Prefer using AELF_PRIVATE_KEY env var instead."
104
+ "description": "aelf wallet private key. Prefer using AELF_PRIVATE_KEY (EOA) or PORTKEY_PRIVATE_KEY (CA) env var instead."
105
105
  },
106
106
  "dryRun": {
107
107
  "type": "boolean",
@@ -156,7 +156,7 @@
156
156
  "privateKey": {
157
157
  "type": "string",
158
158
  "required": false,
159
- "description": "aelf wallet private key. Prefer using AELF_PRIVATE_KEY env var instead."
159
+ "description": "aelf wallet private key. Prefer using AELF_PRIVATE_KEY (EOA) or PORTKEY_PRIVATE_KEY (CA) env var instead."
160
160
  },
161
161
  "dryRun": {
162
162
  "type": "boolean",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eforest-finance/agent-skills",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "AI Agent Kit for aelf token lifecycle on eForest: buy-seed, create-token, issue-token. Supports CLI, MCP, and SDK.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -40,6 +40,7 @@
40
40
  },
41
41
  "dependencies": {
42
42
  "@modelcontextprotocol/sdk": "^1.12.1",
43
+ "@portkey/aelf-signer": "^1.0.0",
43
44
  "aelf-sdk": "^3.2.44",
44
45
  "axios": "^1.7.0",
45
46
  "commander": "^12.1.0",
package/src/core/issue.ts CHANGED
@@ -133,7 +133,6 @@ export async function issueToken(
133
133
  multiTokenAddr,
134
134
  'GetTokenInfo',
135
135
  { symbol: opts.symbol },
136
- config.wallet,
137
136
  );
138
137
  proxyIssuerAddress =
139
138
  typeof tokenInfo?.issuer === 'string' ? tokenInfo.issuer : '';
@@ -151,7 +150,6 @@ export async function issueToken(
151
150
  proxyAddr,
152
151
  'GetProxyAccountByProxyAccountAddress',
153
152
  proxyIssuerAddress,
154
- config.wallet,
155
153
  );
156
154
 
157
155
  const proxyAccountHash = proxyAccountResult?.proxyAccountHash;
@@ -175,7 +173,7 @@ export async function issueToken(
175
173
  methodName: 'Issue',
176
174
  args: Buffer.from(encodedArgs).toString('base64'),
177
175
  },
178
- config.wallet,
176
+ config.signer,
179
177
  );
180
178
 
181
179
  return {
package/src/core/seed.ts CHANGED
@@ -170,7 +170,6 @@ export async function buySeed(
170
170
  multiTokenAddr,
171
171
  'GetBalance',
172
172
  { symbol: 'ELF', owner: config.walletAddress },
173
- config.wallet,
174
173
  );
175
174
  const balanceELF =
176
175
  Number(balance?.balance ?? 0) / 10 ** ELF_DECIMALS;
@@ -193,7 +192,6 @@ export async function buySeed(
193
192
  owner: config.walletAddress,
194
193
  spender: contractAddr,
195
194
  },
196
- config.wallet,
197
195
  );
198
196
  if (Number(allowance?.allowance ?? 0) < priceInSmallUnits) {
199
197
  await callContractSend(
@@ -205,7 +203,7 @@ export async function buySeed(
205
203
  symbol: 'ELF',
206
204
  amount: String(priceInSmallUnits),
207
205
  },
208
- config.wallet,
206
+ config.signer,
209
207
  );
210
208
  }
211
209
  }
@@ -216,7 +214,7 @@ export async function buySeed(
216
214
  contractAddr,
217
215
  'Buy',
218
216
  { symbol: params.symbol, issueTo: params.issueTo },
219
- config.wallet,
217
+ config.signer,
220
218
  );
221
219
 
222
220
  const seedSymbol = parseSeedSymbolFromLogs(result.txResult?.Logs);
package/src/core/token.ts CHANGED
@@ -127,7 +127,6 @@ export async function createToken(
127
127
  owner: config.walletAddress,
128
128
  spender: tokenAdapterAddr,
129
129
  },
130
- config.wallet,
131
130
  );
132
131
 
133
132
  // Approve if needed
@@ -141,7 +140,7 @@ export async function createToken(
141
140
  symbol: opts.seedSymbol,
142
141
  amount: '1',
143
142
  },
144
- config.wallet,
143
+ config.signer,
145
144
  );
146
145
  }
147
146
 
@@ -151,7 +150,7 @@ export async function createToken(
151
150
  tokenAdapterAddr,
152
151
  'CreateToken',
153
152
  createParams,
154
- config.wallet,
153
+ config.signer,
155
154
  );
156
155
 
157
156
  const createTxId = createResult.TransactionId;
@@ -164,7 +163,6 @@ export async function createToken(
164
163
  multiTokenAddr,
165
164
  'GetTokenInfo',
166
165
  { symbol: opts.symbol },
167
- config.wallet,
168
166
  );
169
167
  proxyIssuer =
170
168
  typeof tokenInfo?.issuer === 'string' ? tokenInfo.issuer : '';
package/src/mcp/server.ts CHANGED
@@ -4,22 +4,18 @@
4
4
  *
5
5
  * Registers core functions as Model Context Protocol (MCP) tools.
6
6
  * Each tool maps to a core function with Zod input validation.
7
+ * Supports both EOA and CA (Portkey) wallets via @portkey/aelf-signer.
7
8
  *
8
9
  * Usage:
9
10
  * bun run src/mcp/server.ts # stdio transport (default)
10
11
  * EFOREST_NETWORK=testnet bun run src/mcp/server.ts
11
12
  *
12
- * MCP config example (for AI clients):
13
- * {
14
- * "mcpServers": {
15
- * "eforest-token": {
16
- * "command": "bun",
17
- * "args": ["run", "src/mcp/server.ts"],
18
- * "cwd": "<path-to-skills-dir>",
19
- * "env": { "AELF_PRIVATE_KEY": "xxx", "EFOREST_NETWORK": "mainnet" }
20
- * }
21
- * }
22
- * }
13
+ * MCP config example EOA mode:
14
+ * { "env": { "AELF_PRIVATE_KEY": "xxx", "EFOREST_NETWORK": "mainnet" } }
15
+ *
16
+ * MCP config example — CA (Portkey) mode:
17
+ * { "env": { "PORTKEY_PRIVATE_KEY": "xxx", "PORTKEY_CA_HASH": "xxx",
18
+ * "PORTKEY_CA_ADDRESS": "xxx", "EFOREST_NETWORK": "mainnet" } }
23
19
  */
24
20
 
25
21
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
@@ -72,7 +68,8 @@ const server = new McpServer({
72
68
  // --- aelf-buy-seed ---
73
69
  server.tool(
74
70
  'aelf-buy-seed',
75
- `Purchase a SEED on aelf MainChain. Returns seedSymbol (e.g. "SEED-321") needed for create-token.
71
+ `Purchase a SEED on aelf MainChain. Supports both EOA and CA (Portkey) wallets.
72
+ Returns seedSymbol (e.g. "SEED-321") needed for create-token.
76
73
  Performs pre-flight availability check, ELF balance check, and Approve before Buy.
77
74
  Price safety: requires --force or force param. Use force=2 for max 2 ELF.`,
78
75
  {
@@ -120,6 +117,7 @@ Price safety: requires --force or force param. Use force=2 for max 2 ELF.`,
120
117
  server.tool(
121
118
  'aelf-create-token',
122
119
  `Create a new FT token on aelf using an owned SEED (from buy-seed output seedSymbol).
120
+ Supports both EOA and CA (Portkey) wallets.
123
121
  Handles SEED Approve, TokenAdapter.CreateToken, backend save, and cross-chain sync.
124
122
  Returns proxyIssuer (proxy account address) needed for issue-token.
125
123
  Cross-chain sync has graceful degradation: success=true even if sync times out.`,
@@ -172,7 +170,7 @@ Cross-chain sync has graceful degradation: success=true even if sync times out.`
172
170
  // --- aelf-issue-token ---
173
171
  server.tool(
174
172
  'aelf-issue-token',
175
- `Issue tokens to an address via Proxy ForwardCall.
173
+ `Issue tokens to an address via Proxy ForwardCall. Supports both EOA and CA (Portkey) wallets.
176
174
  Because TokenAdapter creates a proxy account as on-chain issuer, this routes through ProxyContract.
177
175
  Auto-detects proxyIssuer from GetTokenInfo if not provided.
178
176
  Steps: GetTokenInfo → GetProxyAccount → encode IssueInput → ForwardCall.`,