Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,39 @@
document.documentElement.style.visibility = 'visible';
})();
</script>
<script>
(function () {
const path = window.location.pathname;
const match = path.match(/^\/n\/([^/]+)$/);

if (match) {
const name = decodeURIComponent(match[1]);
const title = `${name} — Wraith Name`;
const description = `Send payments to ${name} on Wraith Protocol.`;
const url = `${window.location.origin}${path}`;

document.title = title;

const setMeta = (attribute, key, value) => {
let meta = document.head.querySelector(`meta[${attribute}="${key}"]`);
if (!meta) {
meta = document.createElement('meta');
meta.setAttribute(attribute, key);
document.head.appendChild(meta);
}
meta.setAttribute('content', value);
};

setMeta('property', 'og:title', title);
setMeta('property', 'og:description', description);
setMeta('property', 'og:url', url);
setMeta('property', 'og:image', '/og-image.png');
setMeta('name', 'twitter:title', title);
setMeta('name', 'twitter:description', description);
setMeta('name', 'twitter:image', '/og-image.png');
}
})();
</script>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />

Expand Down
2 changes: 2 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { useNotificationSW } from '@/hooks/useNotificationSW';
import Schedule from '@/pages/Schedule';
import StellarSplit from '@/pages/StellarSplit';
import Names from '@/pages/Names';
import NameProfile from '@/pages/NameProfile';
import NamesAuctions from '@/pages/NamesAuctions';
import Activity from '@/pages/Activity';
import Portfolio from '@/pages/Portfolio';
Expand Down Expand Up @@ -187,6 +188,7 @@ export function App() {
<Route path="/stellar/split" element={<StellarSplit />} />
<Route path="/pay" element={<Send />} />
<Route path="/names" element={<Names />} />
<Route path="/n/:name" element={<NameProfile />} />
<Route path="/names/auctions" element={<NamesAuctions />} />
<Route path="/activity" element={<Activity />} />
<Route path="/history" element={<Activity />} />
Expand Down
215 changes: 195 additions & 20 deletions src/lib/stellar/names.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export interface NameMetadata {
avatar_url?: string;
twitter_handle?: string;
description?: string;
socials?: Record<string, string>;
}

export interface NameRecord {
Expand Down Expand Up @@ -48,6 +49,169 @@ export interface MetadataParams {
metadata: NameMetadata;
}

function asString(value: unknown): string | undefined {
if (value === undefined || value === null) return undefined;
if (typeof value === 'string') return value;
if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') {
return String(value);
}
if (typeof value === 'object' && 'toString' in value && typeof value.toString === 'function') {
const text = value.toString();
return text && text !== '[object Object]' ? text : undefined;
}
return undefined;
}

function asNumber(value: unknown): number | undefined {
if (typeof value === 'number' && Number.isFinite(value)) return value;
if (typeof value === 'bigint') return Number(value);
if (typeof value === 'string') {
const parsed = Number(value);
if (Number.isFinite(parsed)) return parsed;
}
return undefined;
}

function normalizeMetadataValue(value: unknown): NameMetadata {
if (!value || typeof value !== 'object') return {};

if (value instanceof Map) {
const entries = Object.fromEntries(
Array.from(value.entries()).map(([key, entryValue]) => [String(key), entryValue]),
);
return normalizeMetadataValue(entries);
}

const record = value as Record<string, unknown>;
const direct = record.metadata ?? record.meta ?? record.attributes ?? record;
const values = direct && typeof direct === 'object' ? (direct as Record<string, unknown>) : {};

const metadata: NameMetadata = {};
const fallbackValues = Object.entries(values).reduce<Record<string, unknown>>(
(acc, [key, entryValue]) => {
if (
typeof entryValue === 'object' &&
entryValue &&
'key' in entryValue &&
'value' in entryValue
) {
acc[String((entryValue as { key?: unknown }).key)] = (
entryValue as { value?: unknown }
).value;
return acc;
}
acc[key] = entryValue;
return acc;
},
{},
);

const avatarUrl = asString(
fallbackValues.avatar_url ?? fallbackValues.avatarUrl ?? fallbackValues.avatar,
);
const twitterHandle = asString(
fallbackValues.twitter_handle ?? fallbackValues.twitterHandle ?? fallbackValues.twitter,
);
const description = asString(
fallbackValues.description ?? fallbackValues.bio ?? fallbackValues.summary,
);
const socials =
typeof fallbackValues.socials === 'object' && fallbackValues.socials
? (fallbackValues.socials as Record<string, unknown>)
: undefined;

if (avatarUrl) metadata.avatar_url = avatarUrl;
if (twitterHandle) metadata.twitter_handle = twitterHandle;
if (description) metadata.description = description;
if (socials) {
metadata.socials = Object.fromEntries(
Object.entries(socials)
.filter(([, v]) => typeof v !== 'undefined' && v !== null)
.map(([key, v]) => [key, asString(v) ?? String(v)]),
);
}

return metadata;
}

function normalizeNameRecordResult(value: unknown, fallbackName: string): NameRecord | null {
if (value === null || value === undefined || value === false) return null;

const root = (() => {
if (typeof value === 'object' && value && 'record' in value)
return (value as { record: unknown }).record;
if (typeof value === 'object' && value && 'result' in value)
return (value as { result: unknown }).result;
return value;
})();

if (!root || typeof root !== 'object') {
if (typeof root === 'string' || typeof root === 'number' || typeof root === 'bigint') {
return null;
}
return null;
}

const obj = root as Record<string, unknown>;
const owner = asString(
obj.owner ??
obj.owner_address ??
obj.ownerAddress ??
obj.address ??
obj.account ??
obj.recipient,
);
const expiresAt = asNumber(
obj.expires_at ?? obj.expiresAt ?? obj.expiration ?? obj.expiry ?? obj.expires,
);

const metadata = normalizeMetadataValue(obj.metadata ?? obj.meta ?? obj.attributes ?? obj);
const name = asString(obj.name) || fallbackName;

if (
!owner &&
!expiresAt &&
!metadata.avatar_url &&
!metadata.description &&
!metadata.twitter_handle
) {
if (Array.isArray(root)) {
const first = root[0];
if (first && typeof first === 'object') return normalizeNameRecordResult(first, fallbackName);
}
return null;
}

return {
name,
owner: owner || '',
expires_at: expiresAt ? Math.floor(expiresAt) : 0,
metadata,
};
}

function parseSimulationReturnValue(value: unknown, fallbackName: string): NameRecord | null {
if (!value || typeof value !== 'object') return null;

if (Array.isArray(value)) {
for (const item of value) {
const parsed = parseSimulationReturnValue(item, fallbackName);
if (parsed) return parsed;
}
return null;
}

if (value instanceof Map) {
for (const entry of value.values()) {
const parsed = parseSimulationReturnValue(entry, fallbackName);
if (parsed) return parsed;
}
return null;
}

return normalizeNameRecordResult(value, fallbackName);
}

export interface NameAuction {
name: string;
commitEnd: number;
Expand Down Expand Up @@ -300,15 +464,14 @@ export async function checkAvailability(name: string): Promise<boolean> {
.build(),
);

if ('error' in result) {
// If error, name might not exist or other issue - assume unavailable to be safe
return false;
}
if ('error' in result) return false;
if (!result.result || !('retval' in result.result)) return true;

// If result exists, name is registered
return false;
} catch (error) {
// If simulation fails, assume name is available
const value = scValToNative(result.result.retval);
if (value === null || value === undefined || value === false) return true;
const owner = asString(value);
return owner !== undefined && owner !== '' && owner !== 'void';
} catch {
return true;
}
}
Expand Down Expand Up @@ -473,18 +636,13 @@ export async function getNameRecord(name: string): Promise<NameRecord | null> {
.build(),
);

if ('error' in result) {
if ('error' in result || !result.result || !('retval' in result.result)) {
return null;
}

// Parse the result - this depends on actual contract return structure
// For now, return a placeholder
return {
name,
owner: '',
expires_at: 0,
metadata: {},
};
const nativeValue = scValToNative(result.result.retval);
const parsed = parseSimulationReturnValue(nativeValue, name);
return parsed;
} catch {
return null;
}
Expand Down Expand Up @@ -512,12 +670,29 @@ export async function getOwnedNames(ownerAddress: string): Promise<string[]> {
.build(),
);

if ('error' in result) {
if ('error' in result || !result.result || !('retval' in result.result)) {
return [];
}

// Parse the result - this depends on actual contract return structure
// For now, return empty array
const value = scValToNative(result.result.retval);
if (Array.isArray(value)) {
return value
.map((entry) => asString(entry))
.filter((entry): entry is string => Boolean(entry));
}

if (value instanceof Set) {
return Array.from(value)
.map((entry) => asString(entry))
.filter((entry): entry is string => Boolean(entry));
}

if (value instanceof Map) {
return Array.from(value.keys())
.map((entry) => asString(entry))
.filter((entry): entry is string => Boolean(entry));
}

return [];
} catch {
return [];
Expand Down
Loading
Loading