// chitanta.mjs: the RECEIPT of a payment for a person or a company (roadmap point 25): an INSTRUCTION document (payer, payee, the amount // in the asset and in USD at the rate declared as an input, the transaction, the risk verdict with its policy under hash) and the AIP-23 // `settlement` envelope that commits to it (instructionHash = SHA-256 of the canonical instruction; txHash and blockHash of the payment; // chainId, from, to, amount, asset). The envelope is what gets notarized on AereNotary, so the receipt's time is the chain's, read back // under a post-quantum certified anchor by the reference verifier; the instruction is what a reader checks against the chain and against // the risk module. `verificaChitanta` is pure (the chain facts come in as arguments: the transaction and its receipt, fetched by the // caller); every rule carries a PAZA marker for the planting control and is NAMED in the verdict (`reguli`: legatura, suma, politica, // risc, lant), so a reader, or the conformance suite, knows which rule refused, not only that something did. Only node:crypto and proof-kinds. import crypto from 'node:crypto'; import { buildProof } from '../proof-kinds/proof-kinds.mjs'; import { canonical, hashOf, definePolicy, judeca, usdOf, usdText } from './risc.mjs'; export const KIND_INSTRUCTION = 'aere-payment-receipt-instruction'; /** The rules this verifier names. A refusal can only name one of these (the helper throws otherwise), so the list is the whole list. */ export const REGULI = ['legatura', 'suma', 'politica', 'risc', 'lant']; const sha256 = (b) => '0x' + crypto.createHash('sha256').update(b).digest('hex'); const H32 = /^0x[0-9a-f]{64}$/i, low = (x) => String(x || '').toLowerCase(); /** Builds the instruction and its settlement envelope from a payment, its chain facts and an ALLOW verdict. */ export function construiesteChitanta({ chainId, txHash, blockHash, payer, payee, asset, decimals, amount, rate, verdict, policyId, purpose = null, createdAt = new Date().toISOString() }) { if (!verdict || verdict.decision !== 'ALLOW') throw new Error('a receipt is issued only on an ALLOW verdict'); const instruction = { v: 1, kind: KIND_INSTRUCTION, chainId, payer: low(payer), payee: low(payee), asset: low(asset), decimals, amount: String(amount), amountUsd: usdText(usdOf(amount, decimals, rate.usdPerUnit)), rate: { usdPerUnit: rate.usdPerUnit, source: rate.source, at: rate.at }, txHash: low(txHash), blockHash: low(blockHash), risk: { policyId, policyHash: verdict.policyHash, inputsHash: verdict.inputsHash, verdictHash: verdict.verdictHash, decision: verdict.decision, unmeasured: verdict.unmeasured }, purpose, issuedAt: createdAt }; const envelope = buildProof('settlement', { chainId, txHash: low(txHash), blockHash: low(blockHash), from: low(payer), to: low(payee), amount: String(amount), asset: low(asset), instruction: Buffer.from(canonical(instruction), 'utf8'), createdAt }); return { instruction, envelope }; } /** * Verifies a receipt against the policy, the inputs the issuer judged, and (when given) the chain facts. * @param {object} r { instruction, envelope } * @param {object} o { policy, inputs, tx?: {from, to, value(hex|string), blockHash}, receipt?: {status(hex|number), blockHash} } */ export function verificaChitanta(r, o = {}) { const motive = [], reguli = new Set(); const ok = (c, m, regula) => { if (!REGULI.includes(regula)) throw new Error('chitanta: undeclared rule ' + regula); if (!c) { motive.push(m); reguli.add(regula); } return !!c; }; const ins = r && r.instruction, env = r && r.envelope; if (!ins || ins.kind !== KIND_INSTRUCTION || !env || !env.statement) return { verdict: 'INVALID', motive: ['not a receipt (instruction + settlement envelope)'], reguli: ['legatura'] }; const st = env.statement; // the envelope commits to this instruction, and says the same thing const okLegatura = env.kind === 'aere-proof-of-settlement-attestation' && sha256(Buffer.from(JSON.stringify(st), 'utf8')) === low(env.statementHash) && st.instructionHash === sha256(Buffer.from(canonical(ins), 'utf8')) && st.chainId === ins.chainId && low(st.from) === ins.payer && low(st.to) === ins.payee && st.amount === ins.amount && low(st.asset) === ins.asset && low(st.txHash) === ins.txHash && low(st.blockHash) === ins.blockHash /*PAZA:legatura*/; ok(okLegatura, 'the envelope does not commit to this instruction (instructionHash, statementHash or the fields differ)', 'legatura'); ok(H32.test(ins.txHash) && H32.test(ins.blockHash) /*PAZA:legatura*/, 'txHash and blockHash must be 32-byte hashes', 'legatura'); // the USD amount is the amount at the declared rate, exactly try { ok(ins.amountUsd === usdText(usdOf(ins.amount, ins.decimals, ins.rate.usdPerUnit)) /*PAZA:suma*/, 'amountUsd is not amount x the declared rate', 'suma'); } catch (e) { ok(false, 'the amount or the rate is malformed: ' + e.message, 'suma'); } // the policy under the hash is the policy shown if (o.policy) { try { ok(definePolicy(o.policy).policyHash === ins.risk.policyHash /*PAZA:politica*/, 'the policy shown does not hash to the policy the verdict names (a policy changed under its hash)', 'politica'); } catch (e) { ok(false, 'the policy is refused: ' + e.message, 'politica'); } } else ok(false, 'no policy given: the risk verdict cannot be reproduced', 'politica'); // the verdict is reproduced from the same inputs, and it was ALLOW if (o.policy && o.inputs) { try { const v = judeca(o.policy, o.inputs); const okRisc = hashOf(o.inputs) === ins.risk.inputsHash && v.verdictHash === ins.risk.verdictHash && v.decision === 'ALLOW' && ins.risk.decision === 'ALLOW' /*PAZA:risc*/; ok(okRisc, v.decision !== 'ALLOW' ? 'the risk module REFUSES these inputs under this policy: ' + v.reasons.join('; ') : 'the risk verdict does not reproduce from the inputs and the policy (inputsHash or verdictHash differ)', 'risc'); ok(low(o.inputs.payer) === ins.payer && low(o.inputs.payee) === ins.payee && String(o.inputs.amount) === ins.amount && low(o.inputs.asset) === ins.asset && o.inputs.chainId === ins.chainId /*PAZA:risc*/, 'the inputs judged are not the payment of this receipt', 'risc'); ok(o.inputs.rate && o.inputs.rate.usdPerUnit === ins.rate.usdPerUnit && o.inputs.rate.source === ins.rate.source && o.inputs.rate.at === ins.rate.at /*PAZA:risc*/, 'the rate judged is not the rate on the receipt', 'risc'); } catch (e) { ok(false, 'the risk verdict could not be reproduced: ' + e.message, 'risc'); } } else if (o.policy) ok(false, 'no inputs given: the risk verdict cannot be reproduced', 'risc'); // the chain facts, when given: the transaction is the payment, mined, in the block the receipt names if (o.tx) try { const val = typeof o.tx.value === 'string' && o.tx.value.startsWith('0x') ? BigInt(o.tx.value).toString() : String(o.tx.value); const status = o.receipt ? Number(BigInt(o.receipt.status)) : null; const okLant = low(o.tx.from) === ins.payer && low(o.tx.to) === ins.payee && (ins.asset !== 'native' || val === ins.amount) && low(o.tx.blockHash) === ins.blockHash && (status === null || status === 1) && (!o.receipt || low(o.receipt.blockHash) === ins.blockHash) /*PAZA:lant*/; ok(okLant, 'the transaction on chain is not this payment (from, to, value, block or status differ)', 'lant'); } catch (e) { ok(false, 'the transaction facts supplied are malformed: ' + String(e.message).slice(0, 80), 'lant'); } return { verdict: motive.length ? 'INVALID' : 'VALID', motive, reguli: [...reguli].sort(), amountUsd: ins.amountUsd, chainChecked: !!o.tx }; }