Some checks failed
CI / Frontend Lint (push) Has been cancelled
CI / Frontend Type Check (push) Has been cancelled
CI / Frontend Build (push) Has been cancelled
CI / Frontend E2E Tests (push) Has been cancelled
CI / Orchestrator Build (push) Has been cancelled
CI / Contracts Compile (push) Has been cancelled
CI / Contracts Test (push) Has been cancelled
Security Scan / Dependency Vulnerability Scan (push) Has been cancelled
Security Scan / OWASP ZAP Scan (push) Has been cancelled
113 lines
3.8 KiB
TypeScript
113 lines
3.8 KiB
TypeScript
/**
|
|
* MT760 — Issue of a Demand Guarantee / Standby Letter of Credit
|
|
* (arch §4.2 Banking Instrument Layer + §6 Instrument Terms Hash).
|
|
*
|
|
* SWIFT FIN message. This is the issuance leg of the two-phase
|
|
* commit. Output is deterministic so the planHash anchored on-chain
|
|
* can be reproduced by any party with access to the InstrumentTerms.
|
|
*
|
|
* Reference: SWIFT FIN Category 7 User Handbook, MT760 format;
|
|
* Emirates Islamic Bank beneficiary-format SBLC template.
|
|
*/
|
|
|
|
import { createHash } from "crypto";
|
|
import type { InstrumentTerms } from "../../types/plan";
|
|
|
|
export interface Mt760Message {
|
|
sender: string;
|
|
receiver: string;
|
|
messageReference: string;
|
|
fin: string;
|
|
fields: Record<string, string>;
|
|
}
|
|
|
|
function formatAmount(amount: number, currency: string): string {
|
|
// SWIFT FIN amount: 3-letter currency + 15n,2d (max), decimal comma.
|
|
if (amount < 0) throw new Error("MT760: amount must be non-negative");
|
|
return `${currency}${amount.toFixed(2).replace(".", ",")}`;
|
|
}
|
|
|
|
function yyMMdd(iso: string): string {
|
|
// Accept YYYY-MM-DD and return YYMMDD.
|
|
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(iso);
|
|
if (!m) throw new Error(`MT760: expiryDate must be YYYY-MM-DD, got '${iso}'`);
|
|
return `${m[1].slice(2)}${m[2]}${m[3]}`;
|
|
}
|
|
|
|
/**
|
|
* Render an MT760 from an InstrumentTerms record. Uses the
|
|
* block-structured FIN format (Block 1/2/4/5). Tag codes:
|
|
*
|
|
* :20: Transaction reference number
|
|
* :23: Further identification
|
|
* :27: Sequence of total (here: 1/1)
|
|
* :30: Date of issue
|
|
* :40C: Applicable rules (URDG 758, UCP 600)
|
|
* :31D: Date and place of expiry
|
|
* :50: Applicant
|
|
* :52A: Issuing bank (BIC)
|
|
* :59: Beneficiary name + account
|
|
* :32B: Amount
|
|
* :77C: Details of guarantee
|
|
* :72Z: Sender to receiver info
|
|
*/
|
|
export function generateMt760(
|
|
terms: InstrumentTerms,
|
|
opts: { transactionReference: string; issueDate: string },
|
|
): Mt760Message {
|
|
const sender = terms.issuingBankBIC;
|
|
const receiver = terms.beneficiaryBankBIC;
|
|
const field32B = formatAmount(terms.amount, terms.currency);
|
|
const field31D = `${yyMMdd(terms.expiryDate)}${terms.placeOfPresentation.toUpperCase()}`;
|
|
|
|
const fields: Record<string, string> = {
|
|
"20": opts.transactionReference,
|
|
"23": "ISSUE OF STANDBY LETTER OF CREDIT",
|
|
"27": "1/1",
|
|
"30": yyMMdd(opts.issueDate),
|
|
"40C": terms.governingLaw,
|
|
"31D": field31D,
|
|
"50": terms.applicant,
|
|
"52A": terms.issuingBankBIC,
|
|
"59": [terms.beneficiaryName, terms.beneficiaryAccount].filter(Boolean).join("\n"),
|
|
"32B": field32B,
|
|
"77C": [
|
|
`TEMPLATE/${terms.templateRef}`,
|
|
`TEMPLATE_HASH/${terms.templateHash}`,
|
|
`TENOR/${terms.tenor}`,
|
|
].join("\n"),
|
|
"72Z": `GOVLAW/${terms.governingLaw}`,
|
|
};
|
|
|
|
// Build FIN block 4 body with :tag:value sequences.
|
|
const block4 = Object.entries(fields)
|
|
.map(([tag, value]) => `:${tag}:${value}`)
|
|
.join("\n");
|
|
|
|
const block1 = `{1:F01${sender.padEnd(12, "X")}0000000000}`;
|
|
const block2 = `{2:I760${receiver.padEnd(12, "X")}N}`;
|
|
const block4Wrapped = `{4:\n${block4}\n-}`;
|
|
const block5 = `{5:{CHK:${checksum(block4)}}}`;
|
|
|
|
const fin = `${block1}${block2}${block4Wrapped}${block5}`;
|
|
|
|
return { sender, receiver, messageReference: opts.transactionReference, fin, fields };
|
|
}
|
|
|
|
/**
|
|
* Deterministic SHA-256 over the canonical field list. Matches
|
|
* InstrumentTerms.templateHash when all 11 required fields are filled
|
|
* in with the SBLC template values.
|
|
*/
|
|
export function messageHash(msg: Mt760Message): string {
|
|
const canonical = Object.entries(msg.fields)
|
|
.sort(([a], [b]) => a.localeCompare(b))
|
|
.map(([k, v]) => `${k}=${v}`)
|
|
.join("\n");
|
|
return createHash("sha256").update(canonical).digest("hex");
|
|
}
|
|
|
|
function checksum(block4Body: string): string {
|
|
return createHash("sha256").update(block4Body).digest("hex").slice(0, 12).toUpperCase();
|
|
}
|