Skip to content

Commit bab6fc8

Browse files
authored
Feat/add bafin cmtat substandard (#5)
* feat: first working version * fix: different fixes for full workflow running * chore: removed unnecesarry attribution * fix: fixing burng
1 parent 662e4fb commit bab6fc8

90 files changed

Lines changed: 15737 additions & 1418 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/programmable-tokens-frontend/app/admin/security-token/[policyId]/page.tsx

Lines changed: 468 additions & 0 deletions
Large diffs are not rendered by default.

src/programmable-tokens-frontend/app/verify/page.tsx

Lines changed: 54 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,18 @@ import { useEffect, useState, useMemo } from "react";
44
import { PageContainer } from "@/components/layout/page-container";
55
import { Input } from "@/components/ui/input";
66
import { useWallet } from "@/hooks/use-wallet";
7-
import { listKycExtendedTokens, type KycExtendedTokenSummary } from "@/lib/api/kyc-extended";
8-
import { TokenRowVerify } from "@/components/verify/TokenRowVerify";
7+
import { listKycExtendedTokens } from "@/lib/api/kyc-extended";
8+
import { listSecurityTokens } from "@/lib/api/security-token";
9+
import {
10+
TokenRowVerify,
11+
type VerifiableTokenSummary,
12+
} from "@/components/verify/TokenRowVerify";
913
import { EmptyState } from "@/components/verify/EmptyState";
1014

1115
export default function VerifyIndexPage() {
1216
const { connected, wallet } = useWallet();
1317
const [walletAddress, setWalletAddress] = useState<string | null>(null);
14-
const [tokens, setTokens] = useState<KycExtendedTokenSummary[] | null>(null);
18+
const [tokens, setTokens] = useState<VerifiableTokenSummary[] | null>(null);
1519
const [error, setError] = useState<string | null>(null);
1620
const [search, setSearch] = useState("");
1721

@@ -36,13 +40,44 @@ export default function VerifyIndexPage() {
3640
};
3741
}, [connected, wallet]);
3842

39-
// Load token list once on mount.
43+
// Load both kyc-extended AND security-token registrations in parallel and
44+
// merge into a single list tagged with `kind`. Failure on one list doesn't
45+
// hide rows from the other — surface a partial-error notice instead.
4046
useEffect(() => {
4147
let cancelled = false;
42-
listKycExtendedTokens()
43-
.then((list) => {
48+
Promise.allSettled([listKycExtendedTokens(), listSecurityTokens()])
49+
.then(([kycExtRes, secTokRes]) => {
4450
if (cancelled) return;
45-
setTokens(list);
51+
const merged: VerifiableTokenSummary[] = [];
52+
if (kycExtRes.status === "fulfilled") {
53+
for (const t of kycExtRes.value) {
54+
merged.push({
55+
policyId: t.policyId,
56+
displayName: t.displayName,
57+
description: t.description,
58+
kind: "kyc-extended",
59+
});
60+
}
61+
}
62+
if (secTokRes.status === "fulfilled") {
63+
for (const t of secTokRes.value) {
64+
merged.push({
65+
policyId: t.policyId,
66+
displayName: t.displayName,
67+
description: t.description,
68+
kind: "security-token",
69+
requiresReceiverKyc: t.requiresReceiverKyc,
70+
});
71+
}
72+
}
73+
setTokens(merged);
74+
if (kycExtRes.status === "rejected" && secTokRes.status === "rejected") {
75+
setError("Failed to load tokens from both lists");
76+
} else if (kycExtRes.status === "rejected") {
77+
setError("Could not load kyc-extended tokens (security-tokens shown).");
78+
} else if (secTokRes.status === "rejected") {
79+
setError("Could not load security-tokens (kyc-extended shown).");
80+
}
4681
})
4782
.catch((e) => {
4883
if (cancelled) return;
@@ -71,8 +106,9 @@ export default function VerifyIndexPage() {
71106
<header className="space-y-2">
72107
<h1 className="text-3xl font-bold text-white">Verify for a Token</h1>
73108
<p className="text-sm text-dark-300">
74-
Browse all kyc-extended tokens registered on this network. Pick one to
75-
complete KYC — you don&apos;t need to own the token to verify.
109+
Browse every token that supports on-chain KYC enrollment (kyc-extended
110+
and security-token). Pick one to complete KYC — you don&apos;t need to
111+
own the token to verify.
76112
</p>
77113
</header>
78114

@@ -83,25 +119,29 @@ export default function VerifyIndexPage() {
83119
onChange={(e) => setSearch(e.target.value)}
84120
/>
85121

86-
{error && (
122+
{error && tokens === null && (
87123
<EmptyState message={`Error loading tokens: ${error}`} />
88124
)}
89125

126+
{error && tokens !== null && tokens.length > 0 && (
127+
<p className="text-xs text-orange-400 -mt-2">{error}</p>
128+
)}
129+
90130
{!error && filtered === null && (
91131
<EmptyState message="Loading…" />
92132
)}
93133

94-
{!error && filtered && filtered.length === 0 && (
134+
{filtered && filtered.length === 0 && (
95135
<EmptyState message={tokens && tokens.length === 0
96-
? "No kyc-extended tokens registered yet."
136+
? "No kyc-extended or security-token registrations on this network yet."
97137
: "No tokens match your search."} />
98138
)}
99139

100-
{!error && filtered && filtered.length > 0 && (
140+
{filtered && filtered.length > 0 && (
101141
<div className="space-y-3">
102142
{filtered.map((token) => (
103143
<TokenRowVerify
104-
key={token.policyId}
144+
key={`${token.kind}:${token.policyId}`}
105145
token={token}
106146
walletAddress={walletAddress}
107147
/>
Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
"use client";
2+
3+
import { useEffect, useState } from "react";
4+
import { useParams } from "next/navigation";
5+
import { PageContainer } from "@/components/layout/page-container";
6+
import { Card } from "@/components/ui/card";
7+
import { Loader2 } from "lucide-react";
8+
import { useWallet } from "@/hooks/use-wallet";
9+
import { useSecurityTokenMembershipStatus } from "@/hooks/useSecurityTokenMembershipStatus";
10+
import {
11+
listSecurityTokens,
12+
requestSecurityTokenInclusion,
13+
type SecurityTokenSummary,
14+
} from "@/lib/api/security-token";
15+
import { bindSessionToToken } from "@/lib/api/kyc-extended";
16+
import { getTokenContext } from "@/lib/api/protocol";
17+
import { ConnectWalletPrompt } from "@/components/verify/ConnectWalletPrompt";
18+
import { VerifyTokenView } from "@/components/verify/VerifyTokenView";
19+
import { KycVerificationFlow } from "@/components/transfer/KycVerificationFlow";
20+
import { getKeriSessionIdForWallet } from "@/lib/utils/keri-session";
21+
22+
const RENEW_GRACE_MS = 7 * 24 * 60 * 60 * 1000;
23+
24+
/** A token summary surface, displayName-only — kept loose so the page can render
25+
* even when {@code listSecurityTokens()} hasn't returned (e.g. on cold load). */
26+
type DisplayToken = Pick<SecurityTokenSummary, "policyId" | "assetName" | "displayName" | "description">;
27+
28+
type ViewState =
29+
| { kind: "loading" }
30+
| { kind: "wrong-substandard"; substandardId: string }
31+
| { kind: "ready"; token: DisplayToken };
32+
33+
export default function VerifySecurityTokenPage() {
34+
const params = useParams<{ policyId: string }>();
35+
const policyId = params?.policyId ?? "";
36+
const { connected, wallet } = useWallet();
37+
38+
const [viewState, setViewState] = useState<ViewState>({ kind: "loading" });
39+
const [walletAddress, setWalletAddress] = useState<string | null>(null);
40+
const [running, setRunning] = useState(false);
41+
42+
const { status, refresh } = useSecurityTokenMembershipStatus(policyId, walletAddress);
43+
44+
useEffect(() => {
45+
if (!connected || !wallet) {
46+
setWalletAddress(null);
47+
return;
48+
}
49+
let cancelled = false;
50+
wallet.getUsedAddresses()
51+
.then((addrs: string[]) => {
52+
if (cancelled) return;
53+
setWalletAddress(addrs[0] ?? null);
54+
})
55+
.catch(() => {
56+
if (cancelled) return;
57+
setWalletAddress(null);
58+
});
59+
return () => {
60+
cancelled = true;
61+
};
62+
}, [connected, wallet]);
63+
64+
useEffect(() => {
65+
if (!policyId) return;
66+
let cancelled = false;
67+
(async () => {
68+
try {
69+
const ctx = await getTokenContext(policyId);
70+
if (cancelled) return;
71+
if (ctx.substandardId !== "security-token") {
72+
setViewState({ kind: "wrong-substandard", substandardId: ctx.substandardId });
73+
return;
74+
}
75+
const tokens = await listSecurityTokens();
76+
if (cancelled) return;
77+
const token = tokens.find((t) => t.policyId === policyId)
78+
?? { policyId, assetName: "", displayName: policyId.slice(0, 12) + "…", description: null };
79+
setViewState({ kind: "ready", token });
80+
} catch (e) {
81+
if (cancelled) return;
82+
setViewState({
83+
kind: "wrong-substandard",
84+
substandardId: e instanceof Error ? e.message : "Unknown error",
85+
});
86+
}
87+
})();
88+
return () => { cancelled = true; };
89+
}, [policyId]);
90+
91+
// Bind the KERI session to this token so the next proof generation auto-inserts
92+
// the user into the security-token allowlist.
93+
useEffect(() => {
94+
if (viewState.kind !== "ready") return;
95+
if (!walletAddress) return;
96+
bindSessionToToken(policyId).catch((e) => {
97+
// eslint-disable-next-line no-console
98+
console.warn("[verify/security-token] bindSessionToToken failed", e);
99+
});
100+
}, [viewState, walletAddress, policyId]);
101+
102+
// Poll while publish is pending so the user sees the transition without reloading.
103+
useEffect(() => {
104+
if (status.kind !== "publish-pending") return;
105+
const id = setInterval(() => refresh(), 10_000);
106+
return () => clearInterval(id);
107+
}, [status.kind, refresh]);
108+
109+
if (viewState.kind === "loading") {
110+
return (
111+
<PageContainer>
112+
<div className="max-w-2xl mx-auto py-10 flex items-center gap-2 text-sm text-dark-300">
113+
<Loader2 className="h-4 w-4 animate-spin text-primary-400" /> Loading…
114+
</div>
115+
</PageContainer>
116+
);
117+
}
118+
119+
if (viewState.kind === "wrong-substandard") {
120+
return (
121+
<PageContainer>
122+
<div className="max-w-2xl mx-auto py-10">
123+
<Card className="p-6 space-y-2">
124+
<h1 className="text-lg font-semibold text-white">Verification not required</h1>
125+
<p className="text-sm text-dark-300">
126+
This token is not a security-token (substandard:{" "}
127+
<code>{viewState.substandardId}</code>). No verification is needed.
128+
</p>
129+
</Card>
130+
</div>
131+
</PageContainer>
132+
);
133+
}
134+
135+
const { token } = viewState;
136+
137+
if (!walletAddress) {
138+
return (
139+
<PageContainer>
140+
<div className="max-w-2xl mx-auto py-10 space-y-6">
141+
<Header token={token} />
142+
<ConnectWalletPrompt />
143+
</div>
144+
</PageContainer>
145+
);
146+
}
147+
148+
if (running) {
149+
return (
150+
<PageContainer>
151+
<div className="max-w-2xl mx-auto py-10 space-y-6">
152+
<Header token={token} />
153+
<KycVerificationFlow
154+
policyId={policyId}
155+
senderAddress={walletAddress}
156+
forceFresh
157+
onBack={() => setRunning(false)}
158+
onComplete={async (proof) => {
159+
try {
160+
const sessionId = getKeriSessionIdForWallet(walletAddress!);
161+
await requestSecurityTokenInclusion(policyId, {
162+
boundAddress: walletAddress!,
163+
kycSessionId: sessionId,
164+
validUntilMs: proof.validUntilMs,
165+
});
166+
} catch (e) {
167+
console.error("[verify/security-token] requestSecurityTokenInclusion failed", e);
168+
}
169+
setRunning(false);
170+
refresh();
171+
}}
172+
/>
173+
</div>
174+
</PageContainer>
175+
);
176+
}
177+
178+
if (status.kind === "loading") {
179+
return (
180+
<PageContainer>
181+
<div className="max-w-2xl mx-auto py-10 space-y-6">
182+
<Header token={token} />
183+
<Card className="p-6 flex items-center gap-2 text-sm text-dark-300">
184+
<Loader2 className="h-4 w-4 animate-spin text-primary-400" /> Checking your status…
185+
</Card>
186+
</div>
187+
</PageContainer>
188+
);
189+
}
190+
191+
let view: React.ReactNode;
192+
if (status.kind === "verified") {
193+
const showRenew = status.validUntilMs - Date.now() < RENEW_GRACE_MS;
194+
view = (
195+
<VerifyTokenView
196+
token={token}
197+
status="verified"
198+
validUntilMs={status.validUntilMs}
199+
showRenew={showRenew}
200+
onChainSynced={status.onChainSynced}
201+
onStart={() => setRunning(true)}
202+
/>
203+
);
204+
} else if (status.kind === "publish-pending") {
205+
view = (
206+
<VerifyTokenView
207+
token={token}
208+
status="publish-pending"
209+
onStart={() => setRunning(true)}
210+
/>
211+
);
212+
} else if (status.kind === "expired") {
213+
view = (
214+
<VerifyTokenView
215+
token={token}
216+
status="expired"
217+
validUntilMs={status.expiredAtMs || undefined}
218+
onStart={() => setRunning(true)}
219+
/>
220+
);
221+
} else {
222+
view = (
223+
<VerifyTokenView
224+
token={token}
225+
status="not-verified"
226+
onStart={() => setRunning(true)}
227+
/>
228+
);
229+
}
230+
231+
return (
232+
<PageContainer>
233+
<div className="max-w-2xl mx-auto py-10 space-y-6">
234+
<Header token={token} />
235+
{view}
236+
</div>
237+
</PageContainer>
238+
);
239+
}
240+
241+
function Header({ token }: { token: DisplayToken }) {
242+
return (
243+
<div className="space-y-2">
244+
<h1 className="text-3xl font-bold text-white">Verify for {token.displayName}</h1>
245+
<p className="text-xs font-mono text-dark-400 break-all">{token.policyId}</p>
246+
{token.description && <p className="text-sm text-dark-300">{token.description}</p>}
247+
</div>
248+
);
249+
}

0 commit comments

Comments
 (0)