|
1 | 1 | import { ErrorCodes, TelagentError, hashDid, isDidClaw, type AgentDID } from '@telagent/protocol'; |
2 | 2 | import { publicKeyFromDid, bytesToHex } from '@claw-network/core'; |
| 3 | +import { ethers } from 'ethers'; |
3 | 4 |
|
4 | 5 | import type { ClawNetGatewayService, IdentityInfo } from '../clawnet/gateway-service.js'; |
5 | 6 | import type { ManagedClawNetNode } from '../clawnet/managed-node.js'; |
6 | 7 | import type { IdentityCache } from '../storage/identity-cache.js'; |
| 8 | +import { getGlobalLogger } from '../logger.js'; |
| 9 | + |
| 10 | +const logger = getGlobalLogger(); |
| 11 | + |
| 12 | +/** Minimal ABI for on-chain DID registration (no artifacts needed) */ |
| 13 | +const IDENTITY_ABI = [ |
| 14 | + 'function getController(bytes32 didHash) view returns (address)', |
| 15 | + 'function batchRegisterDID(bytes32[] didHashes, bytes[] publicKeys, uint8[] purposes, address[] controllers)', |
| 16 | +]; |
7 | 17 |
|
8 | 18 | export interface ResolvedIdentity { |
9 | 19 | did: AgentDID; |
@@ -99,30 +109,79 @@ export class IdentityAdapterService { |
99 | 109 |
|
100 | 110 | /** |
101 | 111 | * Ensure the node's own DID is registered on-chain. |
102 | | - * Uses the embedded ClawNet node's identity service directly (batchRegisterDID) |
103 | | - * which is the correct internal registration path. |
| 112 | + * Strategy: |
| 113 | + * 1. If a managed ClawNet node is available, use its identity service (batchRegisterDID) |
| 114 | + * 2. Otherwise, fall back to direct ethers.js contract call using CLAW_CHAIN_* env vars |
104 | 115 | */ |
105 | 116 | async ensureRegistered(): Promise<void> { |
106 | 117 | const self = await this.getSelf(); |
107 | 118 | // If controller is a real EVM address (starts with 0x), it's already on-chain |
108 | 119 | if (self.controller.startsWith('0x')) { |
109 | 120 | return; |
110 | 121 | } |
111 | | - if (!this.managedNode) { |
112 | | - throw new Error('No managed ClawNet node — cannot auto-register on-chain'); |
113 | | - } |
114 | 122 | // Convert the multibase public key to 0x-prefixed hex for the smart contract |
115 | 123 | const rawBytes = publicKeyFromDid(self.did); |
116 | 124 | const hexKey = `0x${bytesToHex(rawBytes)}`; |
117 | | - // Register directly via the embedded node's identity service |
118 | | - const controller = await this.managedNode.ensureRegisteredOnChain(self.did, hexKey); |
119 | | - if (!controller) { |
120 | | - throw new Error('Chain identity service unavailable on embedded node'); |
| 125 | + |
| 126 | + if (this.managedNode) { |
| 127 | + // Path A: embedded managed node (local dev) |
| 128 | + const controller = await this.managedNode.ensureRegisteredOnChain(self.did, hexKey); |
| 129 | + if (!controller) { |
| 130 | + throw new Error('Chain identity service unavailable on embedded node'); |
| 131 | + } |
| 132 | + } else { |
| 133 | + // Path B: direct ethers.js call (cloud with standalone ClawNet node) |
| 134 | + await this.registerOnChainDirect(self.did, hexKey); |
121 | 135 | } |
122 | 136 | // Refresh self identity to pick up chain data |
123 | 137 | await this.getSelf(); |
124 | 138 | } |
125 | 139 |
|
| 140 | + /** |
| 141 | + * Register DID on-chain directly using ethers.js and CLAW_CHAIN_* env vars. |
| 142 | + * Used as fallback when no managed ClawNet node is available. |
| 143 | + */ |
| 144 | + private async registerOnChainDirect(did: string, publicKeyHex: string): Promise<void> { |
| 145 | + const rpcUrl = process.env.CLAW_CHAIN_RPC_URL; |
| 146 | + const identityAddr = process.env.CLAW_CHAIN_IDENTITY_CONTRACT; |
| 147 | + const signerEnv = process.env.CLAW_SIGNER_ENV || 'CLAW_PRIVATE_KEY'; |
| 148 | + const privateKey = process.env[signerEnv]; |
| 149 | + |
| 150 | + if (!rpcUrl || !identityAddr || !privateKey) { |
| 151 | + throw new Error( |
| 152 | + 'Missing chain config for direct registration. ' + |
| 153 | + 'Set CLAW_CHAIN_RPC_URL, CLAW_CHAIN_IDENTITY_CONTRACT, and CLAW_SIGNER_ENV/private key.', |
| 154 | + ); |
| 155 | + } |
| 156 | + |
| 157 | + const provider = new ethers.JsonRpcProvider(rpcUrl); |
| 158 | + const wallet = new ethers.Wallet(privateKey, provider); |
| 159 | + const contract = new ethers.Contract(identityAddr, IDENTITY_ABI, wallet); |
| 160 | + const didHash = ethers.keccak256(ethers.toUtf8Bytes(did)); |
| 161 | + |
| 162 | + // Check if already registered on-chain |
| 163 | + try { |
| 164 | + const controller = await contract.getController(didHash); |
| 165 | + if (controller !== ethers.ZeroAddress) { |
| 166 | + logger.info('[telagent] DID already on-chain: %s → controller %s', did, controller); |
| 167 | + return; |
| 168 | + } |
| 169 | + } catch { |
| 170 | + // getController reverts when DID not found — proceed to register |
| 171 | + } |
| 172 | + |
| 173 | + // Register via batchRegisterDID (REGISTRAR_ROLE, no ECDSA sig needed) |
| 174 | + logger.info('[telagent] Direct chain registration: %s → controller %s', did, wallet.address); |
| 175 | + const tx = await contract.batchRegisterDID( |
| 176 | + [didHash], |
| 177 | + [publicKeyHex], |
| 178 | + [0], // authentication purpose |
| 179 | + [wallet.address], |
| 180 | + ); |
| 181 | + await tx.wait(); |
| 182 | + logger.info('[telagent] DID registered on-chain successfully: %s', did); |
| 183 | + } |
| 184 | + |
126 | 185 | async resolve(rawDid: string): Promise<ResolvedIdentity> { |
127 | 186 | if (!isDidClaw(rawDid)) { |
128 | 187 | throw new TelagentError(ErrorCodes.VALIDATION, 'DID must use did:claw format'); |
|
0 commit comments