UNPKG

@ledgerhq/coin-canton

Version:
938 lines (806 loc) 27.8 kB
/** * Canton Testing Utilities for Ed25519 Key Generation * * Provides utilities to generate proper Ed25519 keypairs for testing Canton * onboarding without requiring physical Ledger devices. * The mock signer implements canonical hash algorithm used by the Canton app. */ import crypto from "crypto"; import { decodeDeviceDamlTransaction, decodeDeviceMetadata, decodeNode, decodeInputContract, } from "@ledgerhq/hw-app-canton"; import { CantonPreparedTransaction, CantonUntypedVersionedMessage } from "../types/signer"; // Constants from app-canton const PREPARED_TRANSACTION_HASH_PURPOSE = Buffer.from([0x00, 0x00, 0x00, 0x30]); const HASHING_SCHEME_VERSION = 0x02; const NODE_ENCODING_VERSION = 0x01; const PURPOSE_PUBLIC_KEY_FINGERPRINT = 12; const PURPOSE_TOPOLOGY_TRANSACTION_SIGNATURE = 11; const PURPOSE_MULTI_TOPOLOGY_TRANSACTION_SIGNATURE = 55; const MULTIHASH_SHA256_PREFIX = Buffer.from([0x12, 0x20]); export interface CantonTestKeyPair { publicKeyHex: string; // Ready for Canton Gateway API privateKeyHex: string; // ASN.1 DER encoded private key in hex format privateKeyPem: string; // PEM format for signing operations fingerprint: string; // Canton public key fingerprint (multihash: 1220 + SHA256(publicKey)) sign: (hashHex: string) => string; // Sign transaction hash } /** * Generate fresh Ed25519 keypair */ export function generateMockKeyPair(): CantonTestKeyPair { const { publicKey, privateKey } = crypto.generateKeyPairSync("ed25519"); const publicKeyBuffer = publicKey.export({ type: "spki", format: "der" }); const rawPublicKey = publicKeyBuffer.slice(-32); const publicKeyHex = rawPublicKey.toString("hex"); const privateKeyPem = privateKey.export({ type: "pkcs8", format: "pem" }) as string; const privateKeyDer = privateKey.export({ type: "pkcs8", format: "der" }); const privateKeyHex = privateKeyDer.toString("hex"); // Generate fingerprint: Canton computes SHA256(purpose_bytes + public_key_bytes) const purposeBytes = Buffer.allocUnsafe(4); purposeBytes.writeInt32BE(PURPOSE_PUBLIC_KEY_FINGERPRINT, 0); const hash = crypto.createHash("sha256"); hash.update(purposeBytes); hash.update(rawPublicKey); const hashedContent = hash.digest(); // Multihash encoding: 0x12 (SHA256) + 0x20 (32 bytes) + hash const fingerprintBuffer = Buffer.concat([MULTIHASH_SHA256_PREFIX, hashedContent]); const fingerprint = fingerprintBuffer.toString("hex"); return { publicKeyHex, // 64-char hex string (no 0x prefix) privateKeyHex, // ASN.1 DER encoded private key in hex format privateKeyPem, // PEM format string fingerprint, // Canton format: multihash prefix + SHA256(public key) /** * Sign a transaction hash using proper Ed25519 signature */ sign: (hashHex: string): string => { const hashBuffer = Buffer.from(hashHex, "hex"); const privateKeyObj = crypto.createPrivateKey({ key: privateKeyPem, format: "pem", type: "pkcs8", }); return crypto.sign(null, hashBuffer, privateKeyObj).toString("hex"); }, }; } /** * Verify Ed25519 signature against public key and message hash */ export function verifySignature( publicKeyHex: string, signatureHex: string, messageHashHex: string, ): { isValid: boolean; error?: string; details: Record<string, string | number> } { try { // Clean inputs - remove 0x prefixes if present const cleanPublicKey = publicKeyHex.startsWith("0x") ? publicKeyHex.slice(2) : publicKeyHex; const cleanSignature = signatureHex.startsWith("0x") ? signatureHex.slice(2) : signatureHex; const cleanMessageHash = messageHashHex.startsWith("0x") ? messageHashHex.slice(2) : messageHashHex; const details: Record<string, string | number> = { publicKeyLength: cleanPublicKey.length, signatureLength: cleanSignature.length, messageHashLength: cleanMessageHash.length, }; // Validate input lengths if (cleanPublicKey.length !== 64) { return { isValid: false, error: `Invalid public key length: expected 64 hex chars (32 bytes), got ${cleanPublicKey.length}`, details, }; } // Handle 65-byte signatures (remove recovery ID) let processedSignature = cleanSignature; if (cleanSignature.length === 130) { processedSignature = cleanSignature.slice(2, -2); } else if (cleanSignature.length !== 128) { return { isValid: false, error: `Invalid signature length: expected 128 or 130 hex chars, got ${cleanSignature.length}`, details, }; } // Convert hex to buffers const publicKeyBuffer = Buffer.from(cleanPublicKey, "hex"); const signatureBuffer = Buffer.from(processedSignature, "hex"); const messageBuffer = Buffer.from(cleanMessageHash, "hex"); // Create public key object for verification // Ed25519 public keys need to be wrapped in SPKI format for Node.js crypto const spkiHeader = Buffer.from([ 0x30, 0x2a, // SEQUENCE, length 42 0x30, 0x05, // SEQUENCE, length 5 0x06, 0x03, 0x2b, 0x65, 0x70, // OID for Ed25519 0x03, 0x21, 0x00, // BIT STRING, length 33, no unused bits ]); const spkiPublicKey = Buffer.concat([spkiHeader, publicKeyBuffer]); const publicKeyObj = crypto.createPublicKey({ key: spkiPublicKey, format: "der", type: "spki", }); // Verify signature const isValid = crypto.verify(null, messageBuffer, publicKeyObj, signatureBuffer); return { isValid, details: { ...details, processedSignatureLength: processedSignature.length }, }; } catch (error) { return { isValid: false, error: `Verification failed: ${error instanceof Error ? error.message : String(error)}`, details: { error: String(error) }, }; } } export function createMockSigner(keyPair: CantonTestKeyPair) { return { getAddress: async (_derivationPath: string) => ({ address: `canton_test_${keyPair.fingerprint.slice(-8)}`, publicKey: keyPair.publicKeyHex, }), signTransaction: async ( _derivationPath: string, data: CantonPreparedTransaction | CantonUntypedVersionedMessage | string, ) => { let hashToSign: string; if (typeof data === "object" && "transactions" in data) { // CantonUntypedVersionedMessage - process multiple topology transactions const hashes = data.transactions.map(tx => { const txBytes = typeof tx === "string" ? Buffer.from(tx, "hex") : Buffer.from(tx); return computeCantonHash(PURPOSE_TOPOLOGY_TRANSACTION_SIGNATURE, txBytes); }); // Sort hashes lexicographically in hex format const sortedHashes = hashes.sort((a, b) => a.toString("hex").localeCompare(b.toString("hex")), ); // Concatenate with length prefixes: 4-byte count + for each hash: 4-byte length + 34-byte hash const concatBuffer = Buffer.alloc(4 + sortedHashes.length * (4 + 34)); let offset = 0; // Write count of hashes (4 bytes big-endian) concatBuffer.writeUInt32BE(sortedHashes.length, offset); offset += 4; // Write each hash with length prefix for (const hash of sortedHashes) { concatBuffer.writeUInt32BE(34, offset); // Length is always 34 bytes offset += 4; hash.copy(concatBuffer, offset); offset += 34; } // Compute final multi-hash const finalHash = computeCantonHash( PURPOSE_MULTI_TOPOLOGY_TRANSACTION_SIGNATURE, concatBuffer, ); hashToSign = finalHash.toString("hex"); } else if (typeof data === "object") { const canonicalHash = computeCanonicalHash(data); hashToSign = canonicalHash.toString("hex"); } else { hashToSign = data; } const cleanHash = hashToSign.startsWith("0x") ? hashToSign.slice(2) : hashToSign; const signature = keyPair.sign(cleanHash); return { signature }; }, }; } /** * Compute Canton hash with purpose and multihash encoding */ function computeCantonHash(purpose: number, data: Buffer): Buffer { const purposeBytes = Buffer.allocUnsafe(4); purposeBytes.writeUInt32BE(purpose, 0); const hash = crypto.createHash("sha256"); hash.update(purposeBytes); hash.update(data); const hashedContent = hash.digest(); // Multihash encoding: 0x12 (SHA256) + 0x20 (32 bytes) + hash return Buffer.concat([MULTIHASH_SHA256_PREFIX, hashedContent]); } /** * Compute canonical hash for CantonPreparedTransaction * Implements canonical_hash.c */ function computeCanonicalHash(data: CantonPreparedTransaction): Buffer { // Step 1: Parse DAML transaction const damlTx = decodeDeviceDamlTransaction(data.damlTransaction); // Step 2: Hash transaction (hash_transaction) const txHash = hashTransaction(damlTx); // Step 3: Hash nodes (hash_node) const nodesHash = hashNodes(damlTx, data.nodes); // Step 4: Combine transaction and nodes hash const combinedTxHash = crypto.createHash("sha256"); combinedTxHash.update(txHash); combinedTxHash.update(nodesHash); const finalTxHash = combinedTxHash.digest(); // Step 5: Hash metadata (hash_metadata) const metadata = decodeDeviceMetadata(data.metadata); const metadataHash = hashMetadata(metadata, data.inputContracts); // Step 6: Finalize hash (finalize_hash) return finalizeHash(finalTxHash, metadataHash); } /** * Hash DAML transaction * Implements hash_transaction from canonical_hash.c */ function hashTransaction(damlTx: any): Buffer { const hash = crypto.createHash("sha256"); // Add purpose (PREPARED_TRANSACTION_HASH_PURPOSE = {0x00, 0x00, 0x00, 0x30}) hash.update(PREPARED_TRANSACTION_HASH_PURPOSE); // Encode version string encodeString(hash, damlTx.version || ""); // Encode roots count encodeInt32(hash, damlTx.rootsCount || 0); return hash.digest(); } /** * Hash nodes * Implements hash_node from canonical_hash.c */ function hashNodes(damlTx: any, nodes: Uint8Array[]): Buffer { const hash = crypto.createHash("sha256"); // Process each node for (const nodeBytes of nodes) { const node = decodeNode(nodeBytes); // Check if this is a root node const isRootNode = damlTx.roots?.includes(node.nodeId); // Hash the node (encode_node_id_hash) const nodeHash = hashNodeId(node, damlTx.nodeSeeds || []); if (isRootNode) { // For root nodes, add directly to the hash hash.update(nodeHash); } // Non-root nodes are ignored in this implementation } return hash.digest(); } /** * Hash a single node * Implements encode_node_id_hash from canonical_hash.c */ function hashNodeId(node: any, nodeSeeds: any[]): Buffer { // Create separate hash writer for the node const nodeHash = crypto.createHash("sha256"); // Encode the node based on its type (encode_node) if (node.v1) { if (node.v1.create) { encodeCreateNode(nodeHash, node.v1.create, node.nodeId, nodeSeeds); } else if (node.v1.exercise) { encodeExerciseNode(nodeHash, node.v1.exercise, node.nodeId, nodeSeeds); } else if (node.v1.fetch) { encodeFetchNode(nodeHash, node.v1.fetch); } else if (node.v1.rollback) { encodeRollbackNode(nodeHash, node.v1.rollback); } } return nodeHash.digest(); } /** * Encode create node * Implements encode_create from canonical_hash.c */ function encodeCreateNode(hash: crypto.Hash, create: any, nodeId: string, nodeSeeds: any[]): void { // NODE_ENCODING_VERSION = 0x01 hash.update(Buffer.from([NODE_ENCODING_VERSION])); // lf_version string encodeString(hash, create.lfVersion || ""); // 0x00 byte hash.update(Buffer.from([0x00])); // Optional seed (find_seed) const seed = findNodeSeed(nodeId, nodeSeeds); if (seed) { // Seed is present - encode as hash hash.update(seed); } else { // No seed - encode as optional false hash.update(Buffer.from([0x00])); } // contract_id as hex string encodeHexString(hash, create.contractId || ""); // package_name string encodeString(hash, create.packageName || ""); // template_id identifier if (create.templateId) { encodeIdentifier(hash, create.templateId); } // argument value (this needs proper DAML value encoding) if (create.argument) { encodeValue(hash, create.argument); } // signatories repeated strings if (create.signatories) { encodeRepeatedStrings(hash, create.signatories); } // stakeholders repeated strings if (create.stakeholders) { encodeRepeatedStrings(hash, create.stakeholders); } } /** * Encode exercise node * Implements encode_exercise from canonical_hash.c */ function encodeExerciseNode( hash: crypto.Hash, exercise: any, nodeId: string, nodeSeeds: any[], ): void { // NODE_ENCODING_VERSION = 0x01 hash.update(Buffer.from([NODE_ENCODING_VERSION])); // lf_version string encodeString(hash, exercise.lfVersion || ""); // 0x01 byte hash.update(Buffer.from([0x01])); // Seed is always present for exercise nodes const seed = findNodeSeed(nodeId, nodeSeeds); if (seed) { encodeHash(hash, seed); } else { // This should not happen, but handle gracefully hash.update(Buffer.alloc(32, 0)); } // contract_id as hex string encodeHexString(hash, exercise.contractId || ""); // package_name string encodeString(hash, exercise.packageName || ""); // template_id identifier (required for exercise nodes) if (exercise.templateId) { encodeIdentifier(hash, exercise.templateId); } // choice string encodeString(hash, exercise.choice || ""); // argument value if (exercise.argument) { encodeValue(hash, exercise.argument); } // acting_parties repeated strings if (exercise.actingParties) { encodeRepeatedStrings(hash, exercise.actingParties); } // children repeated strings if (exercise.children) { encodeRepeatedStrings(hash, exercise.children); } // exercise_result optional value if (exercise.exerciseResult) { // Optional present hash.update(Buffer.from([0x01])); encodeValue(hash, exercise.exerciseResult); } else { // Optional not present hash.update(Buffer.from([0x00])); } } /** * Encode fetch node * Implements encode_fetch from canonical_hash.c */ function encodeFetchNode(hash: crypto.Hash, fetch: any): void { // NODE_ENCODING_VERSION = 0x01 hash.update(Buffer.from([NODE_ENCODING_VERSION])); // lf_version string encodeString(hash, fetch.lfVersion || ""); // 0x02 byte hash.update(Buffer.from([0x02])); // contract_id as hex string encodeHexString(hash, fetch.contractId || ""); // package_name string encodeString(hash, fetch.packageName || ""); // template_id identifier if (fetch.templateId) { encodeIdentifier(hash, fetch.templateId); } } /** * Encode rollback node * Implements encode_rollback from canonical_hash.c */ function encodeRollbackNode(hash: crypto.Hash, rollback: any): void { // NODE_ENCODING_VERSION = 0x01 hash.update(Buffer.from([NODE_ENCODING_VERSION])); // lf_version string encodeString(hash, rollback.lfVersion || ""); // 0x03 byte hash.update(Buffer.from([0x03])); // children repeated strings if (rollback.children) { encodeRepeatedStrings(hash, rollback.children); } } /** * Find node seed for a given node ID */ function findNodeSeed(nodeId: string, nodeSeeds: any[]): Buffer | null { const nodeIdNum = Number.parseInt(nodeId, 10); for (const seed of nodeSeeds) { if (seed.nodeId === nodeIdNum) { return Buffer.from(seed.seed); } } return null; } /** * Encode identifier */ function encodeIdentifier(hash: crypto.Hash, identifier: any): void { if (identifier.packageId) { encodeString(hash, identifier.packageId); } if (identifier.moduleName) { splitDotAndEncode(hash, identifier.moduleName); } if (identifier.entityName) { splitDotAndEncode(hash, identifier.entityName); } } /** * Split dot-separated string and encode */ function splitDotAndEncode(hash: crypto.Hash, dotStr: string): void { const parts = dotStr.split("."); encodeInt32(hash, parts.length); for (const part of parts) { encodeInt32(hash, part.length); hash.update(Buffer.from(part, "utf8")); } } /** * Hash metadata * Implements hash_metadata from canonical_hash.c */ function hashMetadata(metadata: any, inputContracts: Uint8Array[]): Buffer { const hash = crypto.createHash("sha256"); // Add purpose (PREPARED_TRANSACTION_HASH_PURPOSE = {0x00, 0x00, 0x00, 0x30}) hash.update(PREPARED_TRANSACTION_HASH_PURPOSE); // Encode metadata (encode_metadata) // Version marker (0x01) hash.update(Buffer.from([0x01])); // Encode submitter info if (metadata.submitterInfo) { // act_as repeated strings if (metadata.submitterInfo.actAs) { encodeRepeatedStrings(hash, metadata.submitterInfo.actAs); } else { encodeInt32(hash, 0); } // command_id string encodeString(hash, metadata.submitterInfo.commandId || ""); } else { // No submitter info - encode empty encodeInt32(hash, 0); encodeString(hash, ""); } // Encode other metadata fields encodeString(hash, metadata.transactionUuid || ""); encodeInt32(hash, metadata.mediatorGroup || 0); encodeString(hash, metadata.synchronizerId || ""); // Encode optional time fields if (metadata.minLedgerEffectiveTime === undefined) { // Optional not present hash.update(Buffer.from([0x00])); } else { // Optional present hash.update(Buffer.from([0x01])); encodeInt64(hash, metadata.minLedgerEffectiveTime); } if (metadata.maxLedgerEffectiveTime === undefined) { // Optional not present hash.update(Buffer.from([0x00])); } else { // Optional present hash.update(Buffer.from([0x01])); encodeInt64(hash, metadata.maxLedgerEffectiveTime); } // Encode submission time and input contracts count encodeInt64(hash, metadata.submissionTime || 0); encodeInt32(hash, metadata.inputContractsCount || 0); // Hash input contracts for (const contract of inputContracts) { const contractHash = hashInputContract(contract); encodeHash(hash, contractHash); } return hash.digest(); } /** * Hash input contract * Implements hash_input_contract from canonical_hash.c */ function hashInputContract(contract: Uint8Array): Buffer { const contractData = decodeInputContract(contract); const hash = crypto.createHash("sha256"); // Encode created_at (int64) encodeInt64(hash, contractData.createdAt || 0); // Hash the contract create node in separate buffer const nodeHash = crypto.createHash("sha256"); // Encode the create node if (contractData.v1) { // Simplified encode_create approach nodeHash.update(Buffer.from([NODE_ENCODING_VERSION])); // NODE_ENCODING_VERSION = 0x01 encodeString(nodeHash, contractData.v1.lfVersion || ""); nodeHash.update(Buffer.from([0x00])); // 0x00 byte nodeHash.update(Buffer.from([0x00])); // No seed for input contracts encodeHexString(nodeHash, contractData.v1.contractId || ""); encodeString(nodeHash, contractData.v1.packageName || ""); if (contractData.v1.templateId) { encodeIdentifier(nodeHash, contractData.v1.templateId); } if (contractData.v1.argument) { encodeValue(nodeHash, contractData.v1.argument); } if (contractData.v1.signatories) { encodeRepeatedStrings(nodeHash, contractData.v1.signatories); } if (contractData.v1.stakeholders) { encodeRepeatedStrings(nodeHash, contractData.v1.stakeholders); } } const contractNodeHash = nodeHash.digest(); encodeHash(hash, contractNodeHash); return hash.digest(); } /** * Finalize hash * Implements finalize_hash from canonical_hash.c */ function finalizeHash(txHash: Buffer, metadataHash: Buffer): Buffer { const hash = crypto.createHash("sha256"); // Add purpose (PREPARED_TRANSACTION_HASH_PURPOSE = {0x00, 0x00, 0x00, 0x30}) hash.update(PREPARED_TRANSACTION_HASH_PURPOSE); // Add hashing scheme version (HASHING_SCHEME_VERSION = 0x02) hash.update(Buffer.from([HASHING_SCHEME_VERSION])); // Add transaction hash (32 bytes) hash.update(txHash); // Add metadata hash (32 bytes) hash.update(metadataHash); return hash.digest(); } // Helper functions for canonical encoding function encodeString(hash: crypto.Hash, value: string): void { const bytes = Buffer.from(value, "utf8"); encodeBytes(hash, bytes); } function encodeInt32(hash: crypto.Hash, value: number | null | undefined): void { const buffer = Buffer.allocUnsafe(4); const safeValue = value ?? 0; buffer.writeUInt32BE(safeValue, 0); hash.update(buffer); } function encodeInt64(hash: crypto.Hash, value: number | null | undefined): void { const buffer = Buffer.allocUnsafe(8); const safeValue = value ?? 0; buffer.writeBigUInt64BE(BigInt(safeValue), 0); hash.update(buffer); } function encodeBytes(hash: crypto.Hash, bytes: Buffer): void { encodeInt32(hash, bytes.length); hash.update(bytes); } function encodeRepeatedStrings(hash: crypto.Hash, strings: string[]): void { encodeInt32(hash, strings.length); for (const str of strings) { encodeString(hash, str); } } function encodeHash(hash: crypto.Hash, hashValue: Buffer): void { hash.update(hashValue); } /** * Encode DAML value * Implements encode_value from canonical_hash.c */ function encodeValue(hash: crypto.Hash, value: any): void { if (!value || typeof value !== "object") { // Handle primitive values if (value === null || value === undefined) { // VALUE_UNIT_TAG = 0x00 hash.update(Buffer.from([0x00])); } else if (typeof value === "boolean") { // VALUE_BOOL_TAG = 0x01 hash.update(Buffer.from([0x01])); encodeBool(hash, value); } else if (typeof value === "number" && Number.isInteger(value)) { // VALUE_INT64_TAG = 0x02 hash.update(Buffer.from([0x02])); encodeInt64(hash, value); } else if (typeof value === "string") { // VALUE_TEXT_TAG = 0x07 hash.update(Buffer.from([0x07])); encodeString(hash, value); } return; } // Handle complex values based on their structure if (value.unit !== undefined) { // VALUE_UNIT_TAG = 0x00 hash.update(Buffer.from([0x00])); } else if (value.bool !== undefined) { // VALUE_BOOL_TAG = 0x01 hash.update(Buffer.from([0x01])); encodeBool(hash, value.bool); } else if (value.int64 !== undefined) { // VALUE_INT64_TAG = 0x02 hash.update(Buffer.from([0x02])); encodeInt64(hash, value.int64); } else if (value.numeric !== undefined) { // VALUE_NUMERIC_TAG = 0x03 hash.update(Buffer.from([0x03])); encodeString(hash, value.numeric); } else if (value.timestamp !== undefined) { // VALUE_TIMESTAMP_TAG = 0x04 hash.update(Buffer.from([0x04])); encodeInt64(hash, value.timestamp); } else if (value.date !== undefined) { // VALUE_DATE_TAG = 0x05 hash.update(Buffer.from([0x05])); encodeInt32(hash, value.date); } else if (value.party !== undefined) { // VALUE_PARTY_TAG = 0x06 hash.update(Buffer.from([0x06])); encodeString(hash, value.party); } else if (value.text !== undefined) { // VALUE_TEXT_TAG = 0x07 hash.update(Buffer.from([0x07])); encodeString(hash, value.text); } else if (value.contractId !== undefined) { // VALUE_CONTRACT_ID_TAG = 0x08 hash.update(Buffer.from([0x08])); encodeHexString(hash, value.contractId); } else if (value.optional !== undefined) { // VALUE_OPTIONAL_TAG = 0x09 hash.update(Buffer.from([0x09])); if (value.optional.value === undefined) { // Optional not present hash.update(Buffer.from([0x00])); } else { // Optional present hash.update(Buffer.from([0x01])); encodeValue(hash, value.optional.value); } } else if (value.list !== undefined) { // VALUE_LIST_TAG = 0x0A hash.update(Buffer.from([0x0a])); if (value.list.elements) { encodeRepeatedValues(hash, value.list.elements); } else { encodeInt32(hash, 0); } } else if (value.textMap !== undefined) { // VALUE_TEXT_MAP_TAG = 0x0B hash.update(Buffer.from([0x0b])); if (value.textMap.entries) { encodeRepeatedTextMapEntries(hash, value.textMap.entries); } else { encodeInt32(hash, 0); } } else if (value.record !== undefined) { // VALUE_RECORD_TAG = 0x0C hash.update(Buffer.from([0x0c])); if (value.record.recordId) { // Optional present hash.update(Buffer.from([0x01])); encodeIdentifier(hash, value.record.recordId); } else { // Optional not present hash.update(Buffer.from([0x00])); } if (value.record.fields) { encodeRepeatedRecordFields(hash, value.record.fields); } else { encodeInt32(hash, 0); } } else if (value.variant !== undefined) { // VALUE_VARIANT_TAG = 0x0D hash.update(Buffer.from([0x0d])); if (value.variant.variantId) { // Optional present hash.update(Buffer.from([0x01])); encodeIdentifier(hash, value.variant.variantId); } else { // Optional not present hash.update(Buffer.from([0x00])); } encodeString(hash, value.variant.constructor || ""); if (value.variant.value) { encodeValue(hash, value.variant.value); } } else if (value.enum !== undefined) { // VALUE_ENUM_TAG = 0x0E hash.update(Buffer.from([0x0e])); if (value.enum.enumId) { // Optional present hash.update(Buffer.from([0x01])); encodeIdentifier(hash, value.enum.enumId); } else { // Optional not present hash.update(Buffer.from([0x00])); } encodeString(hash, value.enum.constructor || ""); } else if (value.genMap === undefined) { // Fallback for unknown value types hash.update(Buffer.from([0x00])); } else { // VALUE_GEN_MAP_TAG = 0x0F hash.update(Buffer.from([0x0f])); if (value.genMap.entries) { encodeRepeatedGenMapEntries(hash, value.genMap.entries); } else { encodeInt32(hash, 0); } } } /** * Encode boolean value */ function encodeBool(hash: crypto.Hash, value: boolean): void { hash.update(Buffer.from([value ? 0x01 : 0x00])); } /** * Encode hex string (for contract IDs) */ function encodeHexString(hash: crypto.Hash, hexStr: string): void { const cleanHex = hexStr.startsWith("0x") ? hexStr.slice(2) : hexStr; const bytes = Buffer.from(cleanHex, "hex"); encodeBytes(hash, bytes); } /** * Encode repeated values */ function encodeRepeatedValues(hash: crypto.Hash, values: any[]): void { encodeInt32(hash, values.length); for (const value of values) { encodeValue(hash, value); } } /** * Encode repeated text map entries */ function encodeRepeatedTextMapEntries(hash: crypto.Hash, entries: any[]): void { encodeInt32(hash, entries.length); for (const entry of entries) { encodeString(hash, entry.key || ""); encodeValue(hash, entry.value); } } /** * Encode repeated record fields */ function encodeRepeatedRecordFields(hash: crypto.Hash, fields: any[]): void { encodeInt32(hash, fields.length); for (const field of fields) { if (field.label) { // Optional present hash.update(Buffer.from([0x01])); encodeString(hash, field.label); } else { // Optional not present hash.update(Buffer.from([0x00])); } encodeValue(hash, field.value); } } /** * Encode repeated gen map entries */ function encodeRepeatedGenMapEntries(hash: crypto.Hash, entries: any[]): void { encodeInt32(hash, entries.length); for (const entry of entries) { encodeValue(hash, entry.key); encodeValue(hash, entry.value); } }