UNPKG

@ledgerhq/coin-canton

Version:
262 lines (224 loc) 7.52 kB
#!/usr/bin/env node /* eslint @typescript-eslint/no-var-requires: off */ /* eslint no-console: off */ import fs from "node:fs"; import path from "node:path"; import { execSync } from "node:child_process"; import https from "node:https"; import { fileURLToPath } from "node:url"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const outputFile = path.join(__dirname, "../src/types/transaction-proto.json"); const tempDir = path.join(__dirname, "temp-proto"); const APP_CANTON_REPO = "LedgerHQ/app-canton"; const APP_CANTON_BRANCH = "develop"; const APP_CANTON_BASE_URL = `https://raw.githubusercontent.com/${APP_CANTON_REPO}/${APP_CANTON_BRANCH}`; const DAML_REPO = "digital-asset/daml"; const DAML_BRANCH = "main"; const DAML_BASE_URL = `https://raw.githubusercontent.com/${DAML_REPO}/${DAML_BRANCH}`; const RESERVED_WORDS = [ { pattern: /\bbool bool\b/g, replacement: "bool bool_" }, { pattern: /\bEnum enum\b/g, replacement: "Enum enum_" }, { pattern: /\bstring constructor = (\d+);/g, replacement: "string constructor_ = $1;" }, ]; function handleFileFinish(file, resolve) { file.close(); resolve(); } function handleFileError(filePath, reject) { return err => { fs.unlink(filePath, () => { /* empty function */ }); reject(err); }; } function handleHttpResponse(url, file, filePath, resolve, reject) { return response => { if (response.statusCode !== 200) { reject(new Error(`Failed to download ${url}: ${response.statusCode}`)); return; } response.pipe(file); file.on("finish", () => handleFileFinish(file, resolve)); file.on("error", handleFileError(filePath, reject)); }; } function downloadFile(url, filePath) { return new Promise((resolve, reject) => { const file = fs.createWriteStream(filePath); https.get(url, handleHttpResponse(url, file, filePath, resolve, reject)).on("error", err => { reject(err instanceof Error ? err : new Error(err)); }); }); } const PROTO_FILES = [ { repoPath: "proto/device.proto", localPath: "device.proto", source: "app-canton", }, { repoPath: "proto/com/daml/ledger/api/v2/value.proto", localPath: "com/daml/ledger/api/v2/value.proto", source: "app-canton", }, { repoPath: "proto/com/daml/ledger/api/v2/value_cb.proto", localPath: "com/daml/ledger/api/v2/value_cb.proto", source: "app-canton", }, { repoPath: "sdk/canton/community/ledger-api/src/main/protobuf/com/daml/ledger/api/v2/interactive/interactive_submission_common_data.proto", localPath: "com/daml/ledger/api/v2/interactive/interactive_submission_common_data.proto", source: "daml", }, { repoPath: "sdk/canton/community/ledger-api/src/main/protobuf/com/daml/ledger/api/v2/interactive/transaction/v1/interactive_submission_data.proto", localPath: "com/daml/ledger/api/v2/interactive/transaction/v1/interactive_submission_data.proto", source: "daml", }, { repoPath: "proto/interactive_submission_data_cb.proto", localPath: "com/daml/ledger/api/v2/interactive/transaction/v1/interactive_submission_data_cb.proto", source: "app-canton", }, { repoPath: "proto/google/protobuf/any.proto", localPath: "google/protobuf/any.proto", source: "app-canton", }, { repoPath: "proto/google/protobuf/duration.proto", localPath: "google/protobuf/duration.proto", source: "app-canton", }, { repoPath: "proto/google/protobuf/empty.proto", localPath: "google/protobuf/empty.proto", source: "app-canton", }, { repoPath: "proto/google/protobuf/timestamp.proto", localPath: "google/protobuf/timestamp.proto", source: "app-canton", }, { repoPath: "proto/google/rpc/error_details.proto", localPath: "google/rpc/error_details.proto", source: "app-canton", }, { repoPath: "proto/google/rpc/status.proto", localPath: "google/rpc/status.proto", source: "app-canton", }, ]; function ensureDirectoryExists(dirPath) { if (!fs.existsSync(dirPath)) { fs.mkdirSync(dirPath, { recursive: true }); } } function getDownloadUrl(protoFile) { const baseUrl = protoFile.source === "daml" ? DAML_BASE_URL : APP_CANTON_BASE_URL; return `${baseUrl}/${protoFile.repoPath}`; } async function downloadProtoFile(protoFile) { const url = getDownloadUrl(protoFile); const localPath = path.join(tempDir, protoFile.localPath); ensureDirectoryExists(path.dirname(localPath)); try { await downloadFile(url, localPath); console.log(`Downloaded: ${protoFile.localPath}`); } catch (error) { console.error(`Failed to download ${protoFile.localPath}:`, error.message); throw error; } } async function downloadProtoFiles() { console.log("Downloading proto files from..."); if (!fs.existsSync(tempDir)) { fs.mkdirSync(tempDir, { recursive: true }); } for (const protoFile of PROTO_FILES) { await downloadProtoFile(protoFile); } console.log("All proto files downloaded successfully!"); } // Replace reserved words to make sure only C allowed names are used // see https://github.com/LedgerHQ/app-canton/blob/develop/proto/proto_gen.sh#L257 function replaceReservedWords(filePath) { if (!fs.existsSync(filePath)) return; let content = fs.readFileSync(filePath, "utf8"); for (const { pattern, replacement } of RESERVED_WORDS) { content = content.replace(pattern, replacement); } fs.writeFileSync(filePath, content); console.log(`Updated field names in ${path.basename(filePath)}`); } // Clean up temp directory function cleanup() { if (fs.existsSync(tempDir)) { fs.rmSync(tempDir, { recursive: true, force: true }); console.log("Cleaned up temporary files"); } } function processProtoFiles() { // Replace field names in proto files replaceReservedWords(path.join(tempDir, "com/daml/ledger/api/v2/value.proto")); replaceReservedWords(path.join(tempDir, "com/daml/ledger/api/v2/value_cb.proto")); } function generateProtobufBindings() { // Sanitize paths to prevent command injection const sanitizedTempDir = path.resolve(tempDir); const sanitizedOutputFile = path.resolve(outputFile); const deviceProtoPath = path.join(sanitizedTempDir, "device.proto"); // Validate that paths exist and are within expected directories if (!fs.existsSync(deviceProtoPath)) { throw new Error(`Device proto file not found: ${deviceProtoPath}`); } if (!sanitizedTempDir.startsWith(path.resolve(__dirname))) { throw new Error("Invalid temp directory path"); } if (!sanitizedOutputFile.startsWith(path.resolve(__dirname, ".."))) { throw new Error("Invalid output file path"); } // Use npx from the same Node.js installation directory to avoid PATH manipulation const npxPath = path.join(process.execPath, "..", "npx"); const absoluteNpxPath = path.resolve(npxPath); execSync( absoluteNpxPath, [ "pbjs", "-t", "json", "-w", "es6", "--path", sanitizedTempDir, "-o", sanitizedOutputFile, deviceProtoPath, ], { stdio: "inherit", cwd: __dirname, }, ); } try { console.log("Generating protobuf bindings..."); await downloadProtoFiles(); processProtoFiles(); generateProtobufBindings(); console.log("Protobuf bindings generated successfully!"); } catch (error) { console.error("Error generating protobuf bindings:", error.message); process.exit(1); } finally { cleanup(); }