forked from Dshield-xyz/Dshield
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathreport.ts
More file actions
178 lines (165 loc) · 6.41 KB
/
Copy pathreport.ts
File metadata and controls
178 lines (165 loc) · 6.41 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
import * as StellarSdk from "@stellar/stellar-sdk";
import { POOL_CONTRACT_ID, queryContract } from "./stellar";
import { computeCommitment, computeNullifierHash } from "./poseidon2";
import { fetchCommitmentsFromChain, lookupNoteTxs } from "./indexer";
import { type ShieldedNote } from "./notes";
import { getNetworkLabel } from "./explorer";
import { stroopsToUsdc } from "./format";
export interface ComplianceReport {
network: string;
poolId: string;
/** 0x-prefixed 32-byte commitment, recomputed from the note. */
commitment: string;
/** 0x-prefixed 32-byte nullifier hash, derived from the note. */
nullifierHash: string;
/** Whether the commitment recomputed from the note matches the note's stored commitment. */
integrityOk: boolean;
/** The commitment was found in the pool's on-chain commitment list. */
depositConfirmed: boolean;
/** Leaf index of the commitment on-chain, or null if not found. */
leafIndex: number | null;
/** The nullifier has been spent on-chain (funds withdrawn). */
withdrawn: boolean;
depositTx: { hash: string; at: string } | null;
withdrawTx: { hash: string; at: string } | null;
generatedAt: number;
}
/**
* Build a compliance report for a note from authoritative on-chain data.
* Deliberately omits amounts, addresses, AND the note itself (nullifier +
* secret): the note is a bearer-spendable credential, so embedding it in a
* report meant to be shared with a third party (an auditor, a PDF export)
* would hand them the ability to withdraw the funds. Reproducing this report
* requires the note out of band, from the holder directly.
*/
export async function buildComplianceReport(
note: ShieldedNote,
): Promise<ComplianceReport> {
const poolId = note.poolId || POOL_CONTRACT_ID;
if (!poolId) throw new Error("No pool configured for this note.");
// Re-derive the commitment and nullifier hash from the note's secrets.
const commitment = await computeCommitment(note.nullifier, note.secret);
const nullifierHash = await computeNullifierHash(note.nullifier);
const commitmentClean = commitment.replace(/^0x/, "").toLowerCase();
const integrityOk =
commitmentClean === note.commitment.replace(/^0x/, "").toLowerCase();
// Deposit confirmation: is the commitment in the pool's authoritative list?
const chainCommitments = await fetchCommitmentsFromChain(poolId);
let leafIndex: number | null = null;
if (chainCommitments) {
const idx = chainCommitments.findIndex(
(c) => c.replace(/^0x/, "").toLowerCase() === commitmentClean,
);
if (idx >= 0) leafIndex = idx;
}
const depositConfirmed = leafIndex !== null;
// Withdrawal status: has the nullifier been spent on-chain?
let withdrawn = false;
const usedVal = await queryContract(poolId, "is_nullifier_used", [
StellarSdk.xdr.ScVal.scvBytes(
Buffer.from(nullifierHash.replace(/^0x/, ""), "hex"),
),
]);
if (usedVal) withdrawn = StellarSdk.scValToNative(usedVal) === true;
// Best-effort: link the actual deposit/withdraw transactions.
const txs = await lookupNoteTxs(poolId, commitment, nullifierHash);
return {
network: getNetworkLabel(),
poolId,
commitment,
nullifierHash,
integrityOk,
depositConfirmed,
leafIndex,
withdrawn,
depositTx: txs.depositTx,
withdrawTx: txs.withdrawTx,
generatedAt: Date.now(),
};
}
/** Render a report as plain text for download / inspection. */
export function formatReportText(r: ComplianceReport): string {
const line = (k: string, v: string) => `${k.padEnd(20)}${v}`;
return [
"DShield Compliance Report",
"=========================",
line("Generated", new Date(r.generatedAt).toISOString()),
line("Network", r.network),
line("Pool contract", r.poolId),
"",
line("Note integrity", r.integrityOk ? "OK (commitment matches)" : "MISMATCH"),
line(
"Deposit",
r.depositConfirmed
? `Confirmed on-chain (leaf #${r.leafIndex})`
: "Not found on-chain",
),
line("Status", r.withdrawn ? "Withdrawn (nullifier spent)" : "In pool (unspent)"),
line("Commitment", r.commitment),
line("Nullifier hash", r.nullifierHash),
r.depositTx
? line("Deposit tx", `${r.depositTx.hash} (${r.depositTx.at})`)
: line("Deposit tx", "n/a (outside event retention)"),
r.withdrawTx
? line("Withdraw tx", `${r.withdrawTx.hash} (${r.withdrawTx.at})`)
: line("Withdraw tx", r.withdrawn ? "n/a (outside event retention)" : "—"),
].join("\n");
}
/** A single row of the history page's activity feed — deposit, withdrawal, or KYC registration. */
export interface ActivityItem {
type: "deposit" | "withdrawal" | "compliance";
timestamp: number;
commitment: string;
amount: string;
poolId?: string;
}
/**
* Column order/meaning for {@link formatActivityCsv} and
* {@link formatActivityJson}: `type` (deposit|withdrawal|compliance), `date`
* (ISO 8601), `amount_usdc` / `amount_stroops` (blank for compliance rows,
* which carry no amount), `commitment` (the KYC hash for compliance rows),
* and `pool_id` (blank for compliance rows).
*/
const ACTIVITY_CSV_COLUMNS = [
"type",
"date",
"amount_usdc",
"amount_stroops",
"commitment",
"pool_id",
] as const;
function csvEscape(value: string): string {
return /[",\n]/.test(value) ? `"${value.replace(/"/g, '""')}"` : value;
}
/** Render activity rows as CSV. See {@link ACTIVITY_CSV_COLUMNS} for the column list. */
export function formatActivityCsv(items: ActivityItem[]): string {
const rows = items.map((item) => {
const isCompliance = item.type === "compliance";
return [
item.type,
new Date(item.timestamp).toISOString(),
isCompliance ? "" : stroopsToUsdc(item.amount),
isCompliance ? "" : item.amount,
item.commitment,
item.poolId ?? "",
]
.map(csvEscape)
.join(",");
});
return [ACTIVITY_CSV_COLUMNS.join(","), ...rows].join("\n");
}
/** Render activity rows as JSON. Same fields as {@link formatActivityCsv}, one object per row. */
export function formatActivityJson(items: ActivityItem[]): string {
const data = items.map((item) => {
const isCompliance = item.type === "compliance";
return {
type: item.type,
date: new Date(item.timestamp).toISOString(),
amountUsdc: isCompliance ? null : stroopsToUsdc(item.amount),
amountStroops: isCompliance ? null : item.amount,
commitment: item.commitment,
poolId: item.poolId ?? null,
};
});
return JSON.stringify(data, null, 2);
}