Skip to content

Commit 3dfd7cc

Browse files
committed
feat(passport): descarga en PDF A4 en vez de nada
El credit passport no tenia descarga. jspdf ya era dependencia y /v3 ya generaba un PDF del badge, asi que se reutiliza ese patron: mismo formato A4 oscuro, mismo acento por tier, para que un partner reciba el mismo artefacto desde cualquiera de las dos superficies. El documento incluye cuenta, edad, operaciones evaluadas, desglose del score, estado del badge on-chain, la escalera de LTV que aplica el controller, y los tres contract IDs para verificarlo. Precision que faltaba: el passport puntua 0-100 y la escalera del contrato usa 0-1000. Poner 'score 62/100' al lado de '>=800 presta al 85%' no se entiende, asi que el PDF nombra las dos escalas y dice cual lee el contrato. Y el pie dice lo que hay que decir: testnet, y que esto es una atestacion, no un credit rating bajo ningun marco regulatorio.
1 parent 3e98ba8 commit 3dfd7cc

1 file changed

Lines changed: 133 additions & 1 deletion

File tree

web/src/app/passport/page.tsx

Lines changed: 133 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,127 @@ export default function PassportPage() {
105105
}
106106
}
107107

108+
/**
109+
* A4 credit passport. Mirrors the badge PDF in /v3 so a partner receives the
110+
* same artefact from either surface — a portable document, not a text dump.
111+
*/
112+
async function downloadPassportPdf() {
113+
if (!profile) return;
114+
const { jsPDF } = await import("jspdf");
115+
const doc = new jsPDF({ unit: "pt", format: "a4" });
116+
const addr = pubkey.trim();
117+
const tier = profile.score.badgeType;
118+
const tierColors: Record<string, [number, number, number]> = {
119+
Gold: [212, 175, 55],
120+
Silver: [192, 192, 192],
121+
Bronze: [205, 127, 50],
122+
None: [80, 80, 80],
123+
};
124+
const [r, g, b] = tierColors[tier] ?? [34, 197, 94];
125+
126+
doc.setFillColor(5, 5, 5);
127+
doc.rect(0, 0, 595, 842, "F");
128+
doc.setFillColor(r, g, b);
129+
doc.rect(0, 0, 595, 8, "F");
130+
131+
doc.setTextColor(34, 197, 94);
132+
doc.setFont("helvetica", "bold");
133+
doc.setFontSize(28);
134+
doc.text("VIGENTE PROTOCOL", 40, 80);
135+
doc.setTextColor(180, 180, 180);
136+
doc.setFont("helvetica", "normal");
137+
doc.setFontSize(11);
138+
doc.text("credit passport · Stellar Soroban testnet", 40, 100);
139+
140+
doc.setTextColor(255, 255, 255);
141+
doc.setFont("helvetica", "bold");
142+
doc.setFontSize(64);
143+
doc.text(tier.toUpperCase(), 40, 200);
144+
doc.setFontSize(14);
145+
doc.setFont("helvetica", "normal");
146+
doc.setTextColor(r, g, b);
147+
doc.text(`tier · score ${profile.score.totalScore} / 100`, 40, 225);
148+
149+
const row = (label: string, value: string, y: number) => {
150+
doc.setTextColor(130, 130, 130);
151+
doc.setFontSize(9);
152+
doc.text(label.toUpperCase(), 40, y);
153+
doc.setTextColor(255, 255, 255);
154+
doc.setFontSize(12);
155+
doc.text(value, 40, y + 16);
156+
};
157+
158+
let y = 285;
159+
row("account", addr, y); y += 46;
160+
row("account age", `${profile.features.account_age_days} days`, y); y += 46;
161+
row("operations evaluated", String(profile.features.ops_evaluated), y); y += 46;
162+
row(
163+
"score breakdown",
164+
`volume ${profile.score.breakdown.volumePoints} · consistency ${profile.score.breakdown.consistencyPoints} · frequency ${profile.score.breakdown.frequencyPoints}`,
165+
y,
166+
);
167+
y += 46;
168+
row(
169+
"on-chain badge",
170+
badge
171+
? `score ${badge.score}${badge.isDefaulted ? " · DEFAULTED" : ""}`
172+
: "not minted",
173+
y,
174+
);
175+
y += 46;
176+
row("issued", new Date().toISOString().replace("T", " ").slice(0, 19) + " UTC", y);
177+
178+
// The limits below are what makes this a credit document rather than a score
179+
// sheet, so they are stated with their source and their caveat.
180+
y += 60;
181+
doc.setDrawColor(40, 40, 40);
182+
doc.line(40, y, 555, y);
183+
y += 24;
184+
doc.setTextColor(130, 130, 130);
185+
doc.setFontSize(9);
186+
doc.text("HOW THIS IS USED", 40, y);
187+
y += 18;
188+
doc.setTextColor(200, 200, 200);
189+
doc.setFontSize(10);
190+
for (const line of [
191+
"A margin controller reads the ON-CHAIN badge score and sets the borrower's",
192+
"loan-to-value from it. On the 0-1000 badge scale: >=800 borrows at 85% of",
193+
"collateral, >=550 at 75%, >=300 at 60%. Below 300 the protocol does not lend.",
194+
"",
195+
"Note the two scales. The profile score above is 0-100 and is the preview",
196+
"computed from public Stellar Horizon activity; the badge score is 0-1000 and is",
197+
"what the contract actually reads.",
198+
"",
199+
"The score is computed off-chain and signed by 3 of 5 independent keys, verified",
200+
"on-chain at mint. The chain verifies the signatures, not the methodology - this",
201+
"document is an attestation, not a credit rating.",
202+
]) {
203+
doc.text(line, 40, y);
204+
y += 14;
205+
}
206+
207+
y += 20;
208+
doc.setTextColor(110, 110, 110);
209+
doc.setFontSize(9);
210+
doc.text("VERIFY", 40, y);
211+
y += 16;
212+
doc.setTextColor(160, 160, 160);
213+
doc.setFontSize(8);
214+
doc.text("badge CDLLO7QEPX2FGOF4VVEV7ISD7PL6FGEBO4N7XMGSIPVULOW43DZRHWVD", 40, y);
215+
y += 12;
216+
doc.text("controller CCZNOV65BYYMJP35CJDBRSUE5S6HRAW4R2MCB7LY4SVOXOHJKWK7OCLJ", 40, y);
217+
y += 12;
218+
doc.text("pool CDYUHA3TPDCAP5FAJMVPMFDW35ZCPSUV2ND2K2G5EB3QYMUDERKPHNUI", 40, y);
219+
y += 12;
220+
doc.text("github.com/zzzbedream/VIGENTE-PROJECT", 40, y);
221+
222+
doc.setTextColor(90, 90, 90);
223+
doc.setFontSize(8);
224+
doc.text("Testnet. Not a credit report under any regulatory framework.", 40, 812);
225+
226+
doc.save(`vigente-passport-${addr.slice(0, 8)}.pdf`);
227+
}
228+
108229
return (
109230
<main
110231
style={{ fontFamily: "var(--font-readex-pro), system-ui, sans-serif" }}
@@ -149,7 +270,18 @@ export default function PassportPage() {
149270
)}
150271

151272
{profile && (
152-
<PassportCard pubkey={pubkey.trim()} profile={profile} badge={badge} />
273+
<>
274+
<div className="flex justify-end mb-3">
275+
<button
276+
type="button"
277+
onClick={() => void downloadPassportPdf()}
278+
className="text-xs border border-white/15 hover:border-[#22c55e]/60 hover:text-[#22c55e] rounded-md px-3 py-1.5 transition-colors"
279+
>
280+
download PDF
281+
</button>
282+
</div>
283+
<PassportCard pubkey={pubkey.trim()} profile={profile} badge={badge} />
284+
</>
153285
)}
154286
</div>
155287
</main>

0 commit comments

Comments
 (0)