npm package: @portkey/aelf-signer
Description: Unified signer interface for aelf blockchain — supports both EOA (direct signing) and CA (Portkey Contract Account via ManagerForwardCall).
Latest version: 1.0.2

# @portkey/aelf-signer

> aelf 区块链统一签名接口 — 同时支持 EOA（直接私钥签名）和 CA（Portkey 合约账户，通过 ManagerForwardCall 代理签名）。

[English](./README.md)

---

## 为什么需要

aelf 上的 DApp skill（如 Awaken DEX、eForest NFT 等）目前只支持 EOA 钱包。Portkey CA 钱包用户无法使用这些 skill，因为 CA 交易需要将每个合约调用包装在 `ManagerForwardCall` 中。

`@portkey/aelf-signer` 提供统一的 `AelfSigner` 接口，DApp skill 接受此接口替代原始的 `wallet: any` 参数。签名细节由 signer 内部处理 — EOA 直接签名，CA 则包装为 ManagerForwardCall — 对 DApp skill 完全透明。

## 安装

```bash
bun add @portkey/aelf-signer
# 或
npm install @portkey/aelf-signer
```

## 快速开始

### 从环境变量自动检测

```typescript
import { createSignerFromEnv, callViewMethod } from '@portkey/aelf-signer';

// 根据环境变量自动创建 EoaSigner 或 CaSigner
const signer = createSignerFromEnv();

console.log(`身份地址: ${signer.address}`);       // EOA 地址或 CA 地址
console.log(`签名密钥: ${signer.keyAddress}`);    // EOA 相同, CA 为 Manager 地址
console.log(`是否 CA: ${signer.isCa}`);

// 发送合约调用（透明处理：EOA 直签或 CA 通过 ManagerForwardCall）
const result = await signer.sendContractCall(
  'https://aelf-public-node.aelf.io',
  tokenContractAddress,
  'Transfer',
  { to: recipient, symbol: 'ELF', amount: '100000000', memo: '' },
);
console.log(`交易 ID: ${result.transactionId}`);

// View 调用不需要 signer
const balance = await callViewMethod(rpcUrl, tokenContract, 'GetBalance', {
  symbol: 'ELF',
  owner: signer.address,
});
```

### EOA 模式

```typescript
import { createEoaSigner } from '@portkey/aelf-signer';

const signer = createEoaSigner(process.env.AELF_PRIVATE_KEY!);
// signer.address === signer.keyAddress === 钱包地址
```

### CA 模式

```typescript
import { createCaSigner } from '@portkey/aelf-signer';

const signer = createCaSigner({
  managerPrivateKey: process.env.PORTKEY_PRIVATE_KEY!,
  caHash: process.env.PORTKEY_CA_HASH!,
  caAddress: process.env.PORTKEY_CA_ADDRESS!,
  // 可选：直接提供以避免 API 查询
  caContractAddress: process.env.PORTKEY_CA_CONTRACT_ADDRESS,
  // 可选：默认为主网
  portkeyApiUrl: 'https://aa-portkey.portkey.finance',
});

// signer.address = CA 地址（链上身份）
// signer.keyAddress = Manager 地址（签名密钥）
```

## 环境变量

### EOA 模式

```bash
# 设置其一：
AELF_PRIVATE_KEY=<64位 hex 私钥>
# 或
PORTKEY_PRIVATE_KEY=<64位 hex 私钥>
```

### CA 模式

```bash
# 三个都必须设置：
PORTKEY_PRIVATE_KEY=<Manager 私钥 hex>
PORTKEY_CA_HASH=<CA hash>
PORTKEY_CA_ADDRESS=<CA 地址>

# 可选：
PORTKEY_CA_CONTRACT_ADDRESS=<目标链的 CA 合约地址>
PORTKEY_API_URL=<Portkey API 地址, 默认主网>
```

检测优先级：
1. 若 `PORTKEY_CA_HASH` + `PORTKEY_CA_ADDRESS` + `PORTKEY_PRIVATE_KEY` 全部设置 -> **CA 模式**
2. 若 `AELF_PRIVATE_KEY` 已设置 -> **EOA 模式**
3. 若仅 `PORTKEY_PRIVATE_KEY` 已设置 -> **EOA 模式**（降级）

## API 参考

### `AelfSigner` 接口

```typescript
interface AelfSigner {
  readonly address: string;      // 身份地址
  readonly keyAddress: string;   // 签名密钥地址
  readonly isCa: boolean;        // 是否 CA 模式

  sendContractCall(
    rpcUrl: string,
    contractAddress: string,
    methodName: string,
    params: Record<string, unknown>,
  ): Promise<SendResult>;

  signMessage(message: string): string;
}
```

- `address`: 链上身份地址。EOA 为钱包地址，CA 为合约账户地址。用于 `owner`、`from` 等字段。
- `keyAddress`: 实际签名密钥的地址。EOA 等于 `address`，CA 为 Manager 地址。用于 API 认证。
- `sendContractCall()`: 发送合约写入调用。EOA 直接签名；CA 将参数 protobuf 编码后包装为 `ManagerForwardCall`。
- `signMessage()`: SHA256 哈希并签名消息。返回 hex 签名。

### 工厂函数

| 函数 | 说明 |
|------|------|
| `createSignerFromEnv()` | 从环境变量自动检测 signer 类型 |
| `createEoaSigner(privateKey)` | 创建 EOA signer |
| `createCaSigner(config)` | 创建 CA signer |
| `isEoaSigner(signer)` | EoaSigner 类型守卫 |
| `isCaSigner(signer)` | CaSigner 类型守卫 |
| `getSigningAddress(signer)` | 获取 `keyAddress` |

### 工具函数

| 函数 | 说明 |
|------|------|
| `callViewMethod(rpcUrl, addr, method, params?)` | 只读合约调用（无需 signer） |
| `pollTxResult(rpcUrl, txId)` | 轮询交易结果 |
| `getAelfInstance(rpcUrl)` | 获取缓存的 AElf SDK 实例 |
| `fetchChainInfo(apiUrl?)` | 从 Portkey API 获取链信息 |
| `clearCaches()` | 清除所有缓存（测试用） |

## DApp Skill 开发者指南

### 改造前（仅支持钱包）

```typescript
// awaken-agent-skills/src/core/trade.ts（改造前）
export async function executeSwap(config, wallet: any, params) {
  const account = wallet.address;  // 只支持 EOA
  await approveToken(config, wallet, ...);
  await callSendMethod(rpcUrl, contract, 'Swap', wallet, args);
}
```

### 改造后（兼容 signer）

```typescript
// awaken-agent-skills/src/core/trade.ts（改造后）
import type { AelfSigner } from '@portkey/aelf-signer';

export async function executeSwap(config, signer: AelfSigner, params) {
  const account = signer.address;  // CA 返回 caAddress, EOA 返回 walletAddress
  await signer.sendContractCall(rpcUrl, tokenContract, 'Approve', { ... });
  await signer.sendContractCall(rpcUrl, swapContract, 'Swap', { ... });
}
```

### MCP 服务集成

```typescript
// awaken-agent-skills/src/mcp/server.ts
import { createSignerFromEnv } from '@portkey/aelf-signer';

// 在 tool handler 中:
const signer = createSignerFromEnv(); // 自动检测 EOA 或 CA
const result = await executeSwap(config, signer, params);
```

## 架构

```
@portkey/aelf-signer
├── src/
│   ├── types.ts          # AelfSigner 接口, 结果类型
│   ├── utils.ts          # AElf SDK 封装, TX 轮询, view 调用
│   ├── eoa-signer.ts     # EoaSigner: 直接私钥签名
│   ├── ca-signer.ts      # CaSigner: ManagerForwardCall + protobuf 编码
│   ├── factory.ts        # createSignerFromEnv, 类型守卫
│   └── index.ts          # 统一导出
└── __tests__/
    └── unit/
        └── signer.test.ts
```

### CaSigner 工作原理

当调用 `CaSigner.sendContractCall(rpcUrl, contractAddr, method, params)` 时：

1. **解析 CA 合约地址** — 从配置获取或通过 Portkey API 自动发现
2. **获取 protobuf 描述** — 目标合约的 `getContractFileDescriptorSet`
3. **查找 input 类型** — 在 protobuf schema 中定位方法的输入消息类型
4. **编码参数** — 应用 aelf-sdk transform + protobuf 编码为字节
5. **调用 ManagerForwardCall** — 在 CA 合约上发起 `{ caHash, contractAddress, methodName, args }`
6. **轮询交易结果** — 等待出块，返回结果

## License

MIT