/** * 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; } 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 = { "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(); }