mirror of
https://github.com/Such-Software/smirk-website.git
synced 2026-10-21 06:28:46 +02:00
The site now reads tips from the v0.3 backend, which does not hold tips created by v0.2.x wallets, so their links said "Tip not found". A 404 from the v0.3 backend now falls back to the legacy backend. Such tips are shown without a claim button, with a note that claiming needs the next wallet update, because a v0.3 wallet cannot claim them yet. Only the fields the page renders are kept from either response.
79 lines
2.8 KiB
TypeScript
79 lines
2.8 KiB
TypeScript
// Public tip lookup across the two backends.
|
|
//
|
|
// Tips created by v0.3 wallets live on the core backend (api.smirk.cash). Tips
|
|
// created by v0.2.x wallets live only on the legacy backend (backend.smirk.cash).
|
|
// The tip page must show both, so a 404 from core falls back to legacy.
|
|
|
|
export type TipSource = 'core' | 'legacy';
|
|
|
|
export interface PublicTipInfo {
|
|
id: string;
|
|
asset: string;
|
|
amount: number;
|
|
status: string;
|
|
created_at: string;
|
|
is_public: boolean;
|
|
funding_confirmations: number;
|
|
confirmations_required: number;
|
|
/** Which backend holds the tip. Only core tips can be claimed by a v0.3 wallet. */
|
|
source: TipSource;
|
|
}
|
|
|
|
export class TipNotFoundError extends Error {
|
|
constructor() {
|
|
super('Tip not found or not available');
|
|
this.name = 'TipNotFoundError';
|
|
}
|
|
}
|
|
|
|
type FetchLike = (url: string) => Promise<{
|
|
ok: boolean;
|
|
status: number;
|
|
json(): Promise<unknown>;
|
|
}>;
|
|
|
|
function toNumber(value: unknown): number {
|
|
return typeof value === 'number' && Number.isFinite(value) ? value : 0;
|
|
}
|
|
|
|
/** Keep only the fields the page renders; never carry key material along. */
|
|
export function normalizePublicTip(raw: unknown, source: TipSource): PublicTipInfo {
|
|
const r = (raw && typeof raw === 'object' ? raw : {}) as Record<string, unknown>;
|
|
if (typeof r.id !== 'string' || typeof r.asset !== 'string' || typeof r.status !== 'string') {
|
|
throw new Error('Failed to get tip info');
|
|
}
|
|
return {
|
|
id: r.id,
|
|
asset: r.asset,
|
|
amount: toNumber(r.amount),
|
|
status: r.status,
|
|
created_at: typeof r.created_at === 'string' ? r.created_at : '',
|
|
is_public: r.is_public === true,
|
|
funding_confirmations: toNumber(r.funding_confirmations),
|
|
confirmations_required: toNumber(r.confirmations_required),
|
|
source,
|
|
};
|
|
}
|
|
|
|
async function errorMessage(res: { json(): Promise<unknown> }): Promise<string> {
|
|
const data = (await res.json().catch(() => ({}))) as { error?: unknown };
|
|
return typeof data.error === 'string' && data.error ? data.error : 'Failed to get tip info';
|
|
}
|
|
|
|
export async function lookupPublicTip(
|
|
tipId: string,
|
|
opts: { apiUrl: string; legacyApiUrl?: string; fetchImpl: FetchLike },
|
|
): Promise<PublicTipInfo> {
|
|
const path = `/api/v1/tips/social/${encodeURIComponent(tipId)}/public`;
|
|
const core = await opts.fetchImpl(`${opts.apiUrl}${path}`);
|
|
if (core.ok) return normalizePublicTip(await core.json(), 'core');
|
|
if (core.status !== 404) throw new Error(await errorMessage(core));
|
|
|
|
const legacyUrl = opts.legacyApiUrl;
|
|
if (!legacyUrl || legacyUrl === opts.apiUrl) throw new TipNotFoundError();
|
|
const legacy = await opts.fetchImpl(`${legacyUrl}${path}`);
|
|
if (legacy.ok) return normalizePublicTip(await legacy.json(), 'legacy');
|
|
if (legacy.status === 404) throw new TipNotFoundError();
|
|
throw new Error(await errorMessage(legacy));
|
|
}
|