114 lines
5.0 KiB
JavaScript
114 lines
5.0 KiB
JavaScript
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<SendJob>(',
|
|
]) {
|
|
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.`,
|
|
);
|