forked from jsnbuchanan/crowd-funder-for-time-pwa
- Add .generated directory for test artifacts - Save key derivation data to key_derivation.json - Save account initialization to account_init.json - Save registration data to registration.json - Save claim details to claim_details.json - Save test environment to test-env.sh - Save contacts data to contacts.json - Add proper error handling for file operations - Improve deeplink test flow with JSON-based data - Add color output and better status messages - Add ADB device detection and fallback to print mode Technical Changes: - Add file system operations with proper error handling - Standardize JSON output format across Python/TypeScript - Update test flow to use generated JSON files - Add proper typing for registration response - Improve error reporting and debug output This improves the test workflow by saving all intermediate data as JSON files that can be used by other test scripts. The deeplink testing now uses this data instead of environment variables for better reliability.
502 lines
14 KiB
TypeScript
502 lines
14 KiB
TypeScript
/**
|
|
* 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
|
|
};
|