import crypto from 'node:crypto'; import fs from 'node:fs'; import path from 'node:path'; import ts from 'typescript'; const root = process.cwd(); const servicePath = path.join(root, 'api/src/send-chain/send-chain.service.ts'); const submissionPath = path.join(root, 'api/src/send-chain/send-submission.service.ts'); const contract = JSON.parse( fs.readFileSync(path.join(root, 'docs/contracts/send-chain-r9-submission.json'), 'utf8'), ); const serviceSource = fs.readFileSync(servicePath, 'utf8'); const submissionSource = fs.readFileSync(submissionPath, 'utf8'); const parse = (filePath, source) => ts.createSourceFile(filePath, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS); const serviceFile = parse(servicePath, serviceSource); const submissionFile = parse(submissionPath, submissionSource); const serviceClass = serviceFile.statements.find( (statement) => ts.isClassDeclaration(statement) && statement.name?.text === 'SendChainService', ); const submissionClass = submissionFile.statements.find( (statement) => ts.isClassDeclaration(statement) && statement.name?.text === 'SendSubmissionService', ); if (!serviceClass || !submissionClass) throw new Error('R9 send-chain facade or submission domain is missing'); const methodsByName = (classNode) => new Map( classNode.members .filter((member) => ts.isMethodDeclaration(member) && member.name && ts.isIdentifier(member.name)) .map((member) => [member.name.text, member]), ); const facadeMethods = methodsByName(serviceClass); const submissionMethods = methodsByName(submissionClass); const domainSources = new Map(); const domainMethods = new Map(); for (const domain of contract.domains) { const filePath = path.join(root, 'api/src/send-chain', domain.file); const source = fs.readFileSync(filePath, 'utf8'); const file = parse(filePath, source); const classNode = file.statements.find( (statement) => ts.isClassDeclaration(statement) && statement.name?.text === domain.className, ); if (!classNode) throw new Error(`R9 domain class missing: ${domain.className}`); const methods = methodsByName(classNode); const migratedCount = contract.methods.filter((method) => method.file === domain.file).length; if (migratedCount !== domain.methodCount) { throw new Error(`R9 domain contract count changed: ${domain.file}`); } domainSources.set(domain.file, { source, file }); domainMethods.set(domain.file, methods); } const combinedDomainSource = [...domainSources.values()].map((item) => item.source).join('\n'); const digest = (value) => crypto.createHash('sha256').update(value.replace(/\r\n/g, '\n')).digest('hex'); const normalizeFacadeSeams = (value) => value.replaceAll('this.facade.', 'this.'); for (const expected of contract.methods) { const domain = domainSources.get(expected.file); const migrated = domainMethods.get(expected.file)?.get(expected.name); if (!migrated?.body) throw new Error(`R9 migrated method missing: ${expected.name}`); if (digest(normalizeFacadeSeams(migrated.body.getText(domain.file))) !== expected.bodySha256) { throw new Error(`R9 migrated method body changed: ${expected.name}`); } const internalFacade = submissionMethods.get(expected.name); if (!internalFacade?.body || !internalFacade.body.getText(submissionFile).includes(`.${expected.name}(`)) { throw new Error(`R9 internal submission facade does not delegate ${expected.name}`); } const facade = facadeMethods.get(expected.name); if (!facade?.body) throw new Error(`R9 stable facade method missing: ${expected.name}`); const body = facade.body.getText(serviceFile); if (!body.includes(`this.submission.${expected.name}(`)) { throw new Error(`R9 facade does not delegate ${expected.name}`); } } for (const retained of [ 'handleSubmitSegmentResult', 'handleSubmitResult', 'intakeReceipt', 'handleReceipt', 'retryMessageIfAllowed', 'recordCmppFailureReceipt', 'refundMessage', 'queueAndTryDownstreamDelivery', ]) { const method = facadeMethods.get(retained); if (!method?.body || method.body.getText(serviceFile).includes('this.submission.')) { throw new Error(`R9 moved R10 behavior out of the facade: ${retained}`); } } for (const invariant of [ "new Logger('SendChainService')", 'this.prisma.$transaction', 'this.callbacks.releaseMessageReservation', 'this.callbacks.recordCmppFailureReceipt', 'GATEWAY_SUBMIT_STREAM', 'await this.facade.publishGatewaySubmitCommand(command);', 'new Worker(', ]) { if (!combinedDomainSource.includes(invariant)) { throw new Error(`R9 submission invariant missing: ${invariant}`); } } const moduleSource = fs.readFileSync(path.join(root, 'api/src/send-chain/send-chain.module.ts'), 'utf8'); if (!moduleSource.includes('providers: [SendChainService]') || moduleSource.includes('SendSubmissionService,')) { throw new Error('R9 changed the public NestJS provider boundary'); } console.log( `R9 send submission verified: ${contract.methods.length} methods across ${contract.domains.length} domains; ` + `${facadeMethods.size} stable facade methods, unchanged bodies and R10 boundaries preserved.`, );