/** * DID Creation and Registration Flow * @author Matthew Raymer * * This module implements the creation and registration of Decentralized Identifiers (DIDs) * with the endorser.ch service. It matches the Python implementation in new_flow.py. */ import { HDNode } from '@ethersproject/hdnode'; import { Wallet } from '@ethersproject/wallet'; import { createJWT, ES256KSigner, SimpleSigner } from 'did-jwt'; import axios from 'axios'; import { mkdir, writeFile } from 'fs/promises'; import { join } from 'path'; // Constants const DEFAULT_ROOT_DERIVATION_PATH = "m/84737769'/0'/0'/0'"; const API_SERVER = "https://test-api.endorser.ch"; const ENDORSER_DID = "did:ethr:0x0000694B58C2cC69658993A90D3840C560f2F51F"; const ENDORSER_PRIVATE_KEY = "2b6472c026ec2aa2c4235c994a63868fc9212d18b58f6cbfe861b52e71330f5b"; interface IKey { id: string; type: string; controller: string; ethereumAddress: string; publicKeyHex: string; privateKeyHex: string; } interface IIdentifier { did: string; keys: IKey[]; services: any[]; } // Create .generated directory if it doesn't exist const GENERATED_DIR = '.generated'; // Initialize directory creation async function initializeDirectories() { try { await mkdir(GENERATED_DIR, { recursive: true }); } catch (err) { console.error('Error creating .generated directory:', err); throw err; } } /** * Generate a new mnemonic seed phrase */ function generateMnemonic(): string { return Wallet.createRandom().mnemonic.phrase; } /** * Derive Ethereum address and keys from a mnemonic phrase */ async function deriveAddress( mnemonic: string, derivationPath: string = DEFAULT_ROOT_DERIVATION_PATH ): Promise<[string, string, string, string]> { mnemonic = mnemonic.trim().toLowerCase(); const hdnode: HDNode = HDNode.fromMnemonic(mnemonic); const rootNode: HDNode = hdnode.derivePath(derivationPath); console.log("\nKey Derivation:"); console.log(` Mnemonic (first 4 words): ${mnemonic.split(' ').slice(0,4).join(' ')}...`); console.log(` Derivation Path: ${derivationPath}`); const privateHex = rootNode.privateKey.substring(2); // remove '0x' const publicHex = rootNode.publicKey.substring(2); // remove '0x' const address = rootNode.address; console.log(` Address: ${address}`); console.log(` Private Key: ${privateHex.substring(0,8)}...`); console.log(` Public Key: ${publicHex.substring(0,8)}...`); // Save derivation data const derivationData = { mnemonic, derivationPath, address, privateKey: privateHex, publicKey: publicHex }; try { await mkdir(GENERATED_DIR, { recursive: true }); await writeFile( join(GENERATED_DIR, 'key_derivation.json'), JSON.stringify(derivationData, null, 2) ); } catch (err) { console.error('Error saving derivation data:', err); } return [address, privateHex, publicHex, derivationPath]; } /** * Create a new DID identifier with associated keys */ function newIdentifier( address: string, publicKeyHex: string, privateKeyHex: string, derivationPath: string ): IIdentifier { const did = `did:ethr:${address}`; const keyId = `${did}#keys-1`; const identifier: IIdentifier = { did, keys: [{ id: keyId, type: "Secp256k1VerificationKey2018", controller: did, ethereumAddress: address, publicKeyHex, privateKeyHex }], services: [] }; return identifier; } /** * Initialize a new DID account or load existing one */ async function initializeAccount(): Promise<IIdentifier> { const mnemonic = generateMnemonic(); const [address, privateHex, publicHex, derivationPath] = await deriveAddress(mnemonic); const identity = newIdentifier(address, publicHex, privateHex, derivationPath); // Format and display account data const accountData = { did: identity.did, identity, mnemonic, derivation_path: derivationPath }; console.log("\nAccount initialized:"); console.log(JSON.stringify(accountData, null, 2)); console.log(); // Save account data await writeFile( join(GENERATED_DIR, 'account_init.json'), JSON.stringify(accountData, null, 2) ); return identity; } /** * Create a signed JWT for DID registration */ async function createEndorserJwt( did: string, privateKeyHex: string, payload: any, subDid?: string, expiresIn: number = 3600 ): Promise<string> { const signer = await SimpleSigner(privateKeyHex); const jwt = await createJWT( payload, { issuer: did, signer, expiresIn, ...(subDid && { subject: subDid }) } ); return jwt; } /** * Register a DID with the endorser service */ async function register( activeDid: string, privateKeyHex: string, apiServer: string = API_SERVER ): Promise<{ success?: boolean; handleId?: string; registrationId?: number; claimId?: string; error?: string; details?: any; }> { console.log("\nEndorser DID:", ENDORSER_DID); console.log("Active DID:", activeDid); const vcPayload = { vc: { "@context": ["https://www.w3.org/2018/credentials/v1"], type: ["VerifiableCredential"], credentialSubject: { "@context": "https://schema.org", "@type": "RegisterAction", "agent": { "identifier": ENDORSER_DID }, "participant": { "identifier": activeDid }, "object": "endorser.ch", "endTime": Math.floor(Date.now() / 1000) + 7 * 24 * 60 * 60 } } }; // Debug output console.log("\nRegistration Claim:"); console.log(JSON.stringify(vcPayload, null, 2)); console.log(); // Sign with endorser's DID and private key const jwtToken = await createEndorserJwt( ENDORSER_DID, ENDORSER_PRIVATE_KEY, vcPayload, activeDid ); // Debug output console.log("Generated JWT:"); console.log(jwtToken); console.log(); // Debug JWT parts const [header, payload, signature] = jwtToken.split('.'); console.log("\nJWT Header:", JSON.parse(Buffer.from(header, 'base64url').toString())); console.log("JWT Payload:", JSON.parse(Buffer.from(payload, 'base64url').toString())); console.log("JWT Signature length:", Buffer.from(signature, 'base64url').length, "bytes"); console.log(); try { const response = await axios.post( `${apiServer}/api/v2/claim`, { jwtEncoded: jwtToken }, { headers: { 'Content-Type': 'application/json' } } ); if (response.status === 200 && response.data.success?.handleId) { // Return all success details const registrationData = { activeDid, jwtToken, response: response.data.success }; await writeFile( join(GENERATED_DIR, 'registration.json'), JSON.stringify(registrationData, null, 2) ); return { success: true, handleId: response.data.success.handleId, registrationId: response.data.success.registrationId, claimId: response.data.success.claimId }; } // Log full error response console.error("\nAPI Error Response:"); console.error(JSON.stringify(response.data, null, 2)); console.error(); // If we have success data but no handleId, it might be a partial success if (response.data.success) { const registrationData = { activeDid, jwtToken, response: response.data.success }; await writeFile( join(GENERATED_DIR, 'registration.json'), JSON.stringify(registrationData, null, 2) ); return { success: true, ...response.data.success }; } else { const registrationData = { activeDid, jwtToken, response: response.data }; await writeFile( join(GENERATED_DIR, 'registration.json'), JSON.stringify(registrationData, null, 2) ); return { error: "Registration failed", details: response.data }; } } catch (error: any) { // Log detailed error information console.error("\nAPI Request Error:"); if (error.response) { console.error("Status:", error.response.status); console.error("Headers:", error.response.headers); console.error("Data:", JSON.stringify(error.response.data, null, 2)); } else if (error.request) { console.error("No response received"); console.error(error.request); } else { console.error("Error setting up request:", error.message); } console.error(); const registrationData = { activeDid, jwtToken, response: { error: `Error submitting claim: ${error.message}` } }; await writeFile( join(GENERATED_DIR, 'registration.json'), JSON.stringify(registrationData, null, 2) ); return { error: `Error submitting claim: ${error.message}` }; } } /** * Fetch claim details from the endorser service */ async function fetchClaim( claimId: string, activeDid: string, privateKeyHex: string, apiServer: string = API_SERVER ): Promise<any> { // Create authentication JWT const authPayload = { intent: 'claim.read', claimId: claimId }; // Sign with the active DID (not endorser DID) const authToken = await createEndorserJwt( activeDid, privateKeyHex, authPayload ); try { const response = await axios.get( `${apiServer}/api/claim/byHandle/${claimId}`, { headers: { 'Authorization': `Bearer ${authToken}`, 'Content-Type': 'application/json' } } ); const claimData = response.data; // Save claim data try { await writeFile( join(GENERATED_DIR, 'claim_details.json'), JSON.stringify({ claim_id: claimId, active_did: activeDid, response: claimData }, null, 2) ); } catch (err) { console.error('Error saving claim data:', err); } return claimData; } catch (error: any) { console.error("\nError fetching claim:"); if (error.response) { console.error("Status:", error.response.status); console.error("Data:", JSON.stringify(error.response.data, null, 2)); } else { console.error(error.message); } // Save error state try { await writeFile( join(GENERATED_DIR, 'claim_details.json'), JSON.stringify({ claim_id: claimId, active_did: activeDid, error: error.message || 'Unknown error', response: error.response?.data }, null, 2) ); } catch (writeErr) { console.error('Error saving claim error:', writeErr); } throw error; } } /** * Generate test environment data for deeplink testing */ async function generateTestEnv( identity: IIdentifier, jwtToken: string, apiServer: string = API_SERVER ): Promise<void> { // Create test data structure const testEnvData = { CONTACT1_DID: identity.did, CONTACT1_KEY: identity.keys[0].privateKeyHex, CONTACT2_DID: `did:ethr:${Wallet.createRandom().address}`, // Generate random DID for contact 2 CONTACT2_KEY: Wallet.createRandom().privateKey.substring(2), // Remove '0x' ISSUER_DID: ENDORSER_DID, ISSUER_KEY: ENDORSER_PRIVATE_KEY, TEST_JWT: jwtToken, API_SERVER: apiServer }; // Write test environment variables const envContent = Object.entries(testEnvData) .map(([key, value]) => `export ${key}="${value}"`) .join('\n'); await writeFile( join(GENERATED_DIR, 'test-env.sh'), envContent + '\n', 'utf-8' ); // Write test contacts data const contactsData = { contacts: [ { did: testEnvData.CONTACT1_DID, name: 'Test Contact 1' }, { did: testEnvData.CONTACT2_DID, name: 'Test Contact 2' } ] }; await writeFile( join(GENERATED_DIR, 'contacts.json'), JSON.stringify(contactsData, null, 2), 'utf-8' ); } /** * Main execution flow */ async function main() { // Initialize directories first await initializeDirectories(); try { // Create a new DID const identity = await initializeAccount(); const activeDid = identity.did; const privateKeyHex = identity.keys[0].privateKeyHex; // Register the DID const result = await register(activeDid, privateKeyHex); console.log("Registration result:", result); // If registration was successful, fetch the claim details if (result.success && result.claimId) { console.log("\nFetching claim details..."); const claimDetails = await fetchClaim( result.claimId, activeDid, privateKeyHex ); console.log("\nClaim Details:"); console.log(JSON.stringify(claimDetails, null, 2)); // Generate test environment data await generateTestEnv(identity, result.claimId); } } catch (error: any) { console.error("Error:", error.message); } } // Run if called directly if (require.main === module) { main().catch(console.error); } export { generateMnemonic, deriveAddress, newIdentifier, initializeAccount, createEndorserJwt, register, fetchClaim, generateTestEnv };