Unified signer interface for the aelf blockchain — supports both EOA (direct signing) and CA (Portkey Contract Account via ManagerForwardCall).
DApp skills on aelf (Awaken DEX, eForest NFT, etc.) currently only work with EOA wallets. Portkey CA wallet users cannot use these skills because CA transactions require wrapping every contract call in ManagerForwardCall.
@portkey/aelf-signer solves this by providing a common AelfSigner interface that DApp skills can accept instead of raw wallet: any. The signer handles signing details internally — EOA signs directly, CA wraps in ManagerForwardCall — making the wallet type completely transparent to the DApp skill.
bun add @portkey/aelf-signer
# or
npm install @portkey/aelf-signerimport { createSignerFromEnv, callViewMethod } from '@portkey/aelf-signer';
// Automatically creates EoaSigner or CaSigner based on env vars
const signer = createSignerFromEnv();
console.log(`Identity: ${signer.address}`); // EOA address or CA address
console.log(`Signing key: ${signer.keyAddress}`); // Same for EOA, Manager address for CA
console.log(`Is CA: ${signer.isCa}`);
// Send a contract call (transparent: EOA direct or CA via ManagerForwardCall)
const result = await signer.sendContractCall(
'https://aelf-public-node.aelf.io',
tokenContractAddress,
'Transfer',
{ to: recipient, symbol: 'ELF', amount: '100000000', memo: '' },
);
console.log(`TX: ${result.transactionId}`);
// View calls don't need a signer
const balance = await callViewMethod(rpcUrl, tokenContract, 'GetBalance', {
symbol: 'ELF',
owner: signer.address,
});import { createEoaSigner } from '@portkey/aelf-signer';
const signer = createEoaSigner(process.env.AELF_PRIVATE_KEY!);
// signer.address === signer.keyAddress === wallet addressimport { 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!,
// Optional: provide directly to avoid API lookup
caContractAddress: process.env.PORTKEY_CA_CONTRACT_ADDRESS,
// Optional: defaults to mainnet
portkeyApiUrl: 'https://aa-portkey.portkey.finance',
});
// signer.address = CA address (on-chain identity)
// signer.keyAddress = Manager address (signing key)# Set one of:
AELF_PRIVATE_KEY=<64-char hex private key>
# or
PORTKEY_PRIVATE_KEY=<64-char hex private key># All three required:
PORTKEY_PRIVATE_KEY=<manager private key hex>
PORTKEY_CA_HASH=<CA hash>
PORTKEY_CA_ADDRESS=<CA address>
# Optional:
PORTKEY_CA_CONTRACT_ADDRESS=<CA contract address on target chain>
PORTKEY_API_URL=<Portkey API URL, defaults to mainnet>Detection priority:
- If
PORTKEY_CA_HASH+PORTKEY_CA_ADDRESS+PORTKEY_PRIVATE_KEYall set -> CA mode - If
AELF_PRIVATE_KEYset -> EOA mode - If only
PORTKEY_PRIVATE_KEYset -> EOA mode (fallback)
interface AelfSigner {
readonly address: string; // Identity address
readonly keyAddress: string; // Signing key address
readonly isCa: boolean; // Whether CA mode
sendContractCall(
rpcUrl: string,
contractAddress: string,
methodName: string,
params: Record<string, unknown>,
): Promise<SendResult>;
signMessage(message: string): string;
}address: The identity on chain. For EOA: wallet address. For CA: CA address. Use forowner,fromfields.keyAddress: The address of the actual signing key. For EOA: same asaddress. For CA: Manager address. Use for API auth.sendContractCall(): Send a state-changing contract call. EOA signs directly; CA wraps inManagerForwardCallwith protobuf encoding.signMessage(): SHA256-hash and sign a message. Returns hex signature.
| Function | Description |
|---|---|
createSignerFromEnv() |
Auto-detect signer type from env vars |
createEoaSigner(privateKey) |
Create EOA signer |
createCaSigner(config) |
Create CA signer |
isEoaSigner(signer) |
Type guard for EoaSigner |
isCaSigner(signer) |
Type guard for CaSigner |
getSigningAddress(signer) |
Get keyAddress |
| Function | Description |
|---|---|
callViewMethod(rpcUrl, addr, method, params?) |
Read-only contract call (no signer needed) |
pollTxResult(rpcUrl, txId) |
Poll for transaction result |
getAelfInstance(rpcUrl) |
Get cached AElf SDK instance |
fetchChainInfo(apiUrl?) |
Fetch chain info from Portkey API |
clearCaches() |
Clear all caches (for testing) |
// awaken-agent-skills/src/core/trade.ts (before)
export async function executeSwap(config, wallet: any, params) {
const account = wallet.address; // Only works with EOA
await approveToken(config, wallet, ...);
await callSendMethod(rpcUrl, contract, 'Swap', wallet, args);
}// awaken-agent-skills/src/core/trade.ts (after)
import type { AelfSigner } from '@portkey/aelf-signer';
export async function executeSwap(config, signer: AelfSigner, params) {
const account = signer.address; // caAddress for CA, walletAddress for EOA
await signer.sendContractCall(rpcUrl, tokenContract, 'Approve', { ... });
await signer.sendContractCall(rpcUrl, swapContract, 'Swap', { ... });
}// awaken-agent-skills/src/mcp/server.ts
import { createSignerFromEnv } from '@portkey/aelf-signer';
// In tool handler:
const signer = createSignerFromEnv(); // Auto-detects EOA or CA
const result = await executeSwap(config, signer, params);@portkey/aelf-signer
├── src/
│ ├── types.ts # AelfSigner interface, result types
│ ├── utils.ts # AElf SDK wrappers, TX polling, view calls
│ ├── eoa-signer.ts # EoaSigner: direct private key signing
│ ├── ca-signer.ts # CaSigner: ManagerForwardCall + protobuf encoding
│ ├── factory.ts # createSignerFromEnv, type guards
│ └── index.ts # Re-exports
└── __tests__/
└── unit/
└── signer.test.ts
When CaSigner.sendContractCall(rpcUrl, contractAddr, method, params) is called:
- Resolve CA contract address — from config or auto-discovered via Portkey API
- Fetch protobuf descriptors —
getContractFileDescriptorSetfor the target contract - Find input type — locate the method's input message type in the protobuf schema
- Encode params — apply aelf-sdk transforms + protobuf encoding to bytes
- Call ManagerForwardCall — on the CA contract with
{ caHash, contractAddress, methodName, args } - Poll TX result — wait for mining, return result
MIT