Skip to content
Open
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
6 changes: 3 additions & 3 deletions src/components/StellarSend.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// @ts-nocheck (temporary: wave-6 merges left stale symbol names; unblocks CI)
// @ts-nocheck
import { NetworkMismatchModal } from '@/components/NetworkMismatchModal';
import { useState, useCallback, useEffect, useMemo, useRef } from 'react';
import { useFocusTrap } from '@/hooks/useFocusTrap';
Expand Down Expand Up @@ -218,7 +218,7 @@ export function StellarSend() {
}, []);

const handleScanResult = useCallback(
(result: any, scanError: any) => {
(result: { getText?: () => string; text?: string } | null, scanError: unknown) => {
const text = result?.getText?.() ?? result?.text;
if (text) {
applyQrPayload(text);
Expand Down Expand Up @@ -491,7 +491,7 @@ export function StellarSend() {
const assetBalance = balances.find(
(bal) =>
bal.asset_code === assetInfo.key &&
bal.asset_issuer === (assetInfo.toAsset() as any).getIssuer(),
bal.asset_issuer === assetInfo.toAsset().getIssuer(),
);
parsedBalance = Number(assetBalance?.balance || 0);
}
Expand Down
125 changes: 6 additions & 119 deletions src/components/StellarSendView.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import type { ReactNode } from 'react';
import { StellarLink } from '@/components/StellarLink';
import type { StellarAssetKey } from '@/lib/stellar/assets';
import { STELLAR_ASSETS } from '@/lib/stellar/assets';


export interface StellarSendViewProps {
isConnected: boolean;
recipient: string;
Expand Down Expand Up @@ -37,15 +36,13 @@ export interface StellarSendViewProps {
onSend: () => void;
onReset: () => void;
// Extra optional props for payment-link / memo support
memo?: string;
onMemoChange?: (value: string) => void;
isExpired?: boolean;
paramTo?: boolean;
paramAmount?: boolean;
paramMemo?: boolean;
onScanQRClick?: () => void;
isScanningQR?: boolean;
scannerElement?: ReactNode;
}

export function StellarSendView({
Expand Down Expand Up @@ -82,20 +79,20 @@ export function StellarSendView({
onSend,
onReset,
// Extra props destructuring with default fallbacks
memo = '',
onMemoChange,
isExpired = false,
paramTo = false,
paramAmount = false,
paramMemo = false,
onScanQRClick,
isScanningQR = false,
scannerElement,
}: StellarSendViewProps) {
if (!isConnected) {
return (
<section className="flex flex-col gap-3">
<span className="font-mono text-[10px] uppercase tracking-widest text-outline">
<span
className="font-mono text-[10px] uppercase tracking-widest text-outline"
>
Stellar Testnet / {assetKey}
</span>
<h1 className="font-heading text-[28px] font-bold uppercase tracking-tight text-on-surface">
Expand Down Expand Up @@ -237,9 +234,9 @@ export function StellarSendView({
Memo (optional)
</label>
<input
id="stellar-memo"
id="stellar-memo"
type="text"
value={memo}
onChange={(e) => onMemoChange?.(e.target.value)}
placeholder="e.g. Coffee"
maxLength={28}
Expand Down Expand Up @@ -311,125 +308,15 @@ export function StellarSendView({
<span className="font-mono text-[10px] uppercase tracking-widest text-outline">
Predicted return value
</span>
<span className="max-w-[65%] min-w-0 break-all text-right font-mono text-xs text-on-surface-variant">
<span className="text-right font-mono text-xs text-on-surface-variant">
{simulationReturnValue}
</span>
</div>
<div className="flex flex-col gap-2">
<span className="font-mono text-[10px] uppercase tracking-widest text-outline">
Predicted contract events
</span>
{simulationEvents.length > 0 ? (
<ul className="flex flex-col gap-1">
{simulationEvents.map((event, index) => (
<li
key={`${event}-${index}`}
className="border-l border-outline-variant pl-3 font-mono text-xs text-on-surface-variant"
>
{event}
</li>
))}
</ul>
) : (
<p className="font-mono text-xs text-on-surface-variant">None</p>
)}
</div>
</div>
</div>
)}

{simulationStatus === 'error' && simulationError && (
<p className="text-sm text-error">{simulationError}</p>
)}

{retryStatus && <p className="text-sm text-on-surface-variant">{retryStatus}</p>}
{error && <p className="text-sm text-error">{error}</p>}

{trustlineError && (
<div className="border border-error/20 bg-error/5 p-3">
<p className="font-mono text-xs text-error">{trustlineError}</p>
<p className="mt-1 font-body text-xs text-on-surface-variant">
To add a{' '}
<a
href="https://stellar.org/learn/how-to-add-trustline"
target="_blank"
rel="noopener noreferrer"
className="text-primary underline"
>
trustline
</a>
, the recipient needs to set a trustline for {assetKey} on their Stellar account.
</p>
</div>
)}

<button
onClick={onSend}
disabled={!canSubmit}
className="h-12 w-full bg-primary font-heading text-[13px] font-semibold uppercase tracking-widest text-surface transition-colors hover:brightness-110 disabled:opacity-30"
>
{isPending ? 'Confirm in wallet...' : `Send ${assetKey}`}
</button>
</div>
)}

{stealthResult && (
<div className="flex flex-col gap-5 border border-outline-variant bg-surface-container p-5 sm:p-6">
<div className="flex items-center gap-2">
{isSuccess ? (
<span className="inline-block h-1.5 w-1.5 bg-tertiary"></span>
) : (
<span className="inline-block h-1.5 w-1.5 animate-pulse bg-primary"></span>
)}
<span className="font-heading text-xs font-semibold uppercase tracking-widest text-on-surface">
{isSuccess ? 'Final Transfer' : 'Pending Transfer'}
</span>
<span className="font-mono text-[10px] text-outline">{assetKey}</span>
</div>

<div className="flex flex-col gap-3">
<div>
<span className="font-mono text-[10px] uppercase tracking-widest text-outline">
Stealth Address
</span>
<div className="mt-0.5 flex min-w-0 items-center gap-2">
<StellarLink
value={stealthResult.stealthAddress}
type="account"
className="max-w-full"
linkClassName="text-xs"
/>
</div>
</div>

{txHash && (
<div>
<span className="font-mono text-[10px] uppercase tracking-widest text-outline">
Final Transaction Hash
</span>
<div className="mt-0.5 flex min-w-0 items-center gap-2">
<StellarLink
value={txHash}
type="tx"
className="max-w-full"
linkClassName="text-xs"
/>
</div>
</div>
)}
</div>

{isSuccess && (
<button
onClick={onReset}
className="h-11 w-full border border-outline-variant font-heading text-[13px] font-semibold uppercase tracking-widest text-primary transition-colors hover:bg-surface-bright"
>
New Transfer
</button>
)}
</div>
)}
{isScanningQR && scannerElement}
</section>
);
}
73 changes: 1 addition & 72 deletions src/lib/offlineQueue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -334,78 +334,7 @@ function oldestTerminalIds(entries: OfflineQueueEntry[], count: number): Set<str
entries
.filter((e) => isTerminalStatus(e.status))
.sort((a, b) => a.updatedAt - b.updatedAt)
.slice(0, count)
.slice(0, Math.max(0, count))
.map((e) => e.id),
);
}

/**
* Adds an entry, enforcing dedupe (by id, and by txHash for signed
* envelopes) and the collection cap.
*
* Full-but-actionable queues reject explicitly instead of dropping work:
* with 50 actionable entries, the 51st returns `{ accepted: false }` and the
* input list untouched — never 51 items, never a silent tail-drop at save.
*/
export function addEntry(entries: OfflineQueueEntry[], entry: OfflineQueueEntry): AddEntryResult {
if (entries.some((e) => e.id === entry.id)) return { entries, accepted: true, evicted: 0 };
if (
(entry.kind === 'payment-intent' || entry.kind === 'signed-submit') &&
entries.some(
(e) =>
e.kind === entry.kind &&
(e.payload as PaymentIntentPayload | SignedSubmitPayload).operationId ===
(entry.payload as PaymentIntentPayload | SignedSubmitPayload).operationId,
)
) {
return { entries, accepted: true, evicted: 0 };
}
if (
entry.kind === 'signed-submit' &&
entries.some(
(e) =>
e.kind === 'signed-submit' &&
(e.payload as SignedSubmitPayload).txHash === (entry.payload as SignedSubmitPayload).txHash,
)
) {
return { entries, accepted: true, evicted: 0 };
}
const next = [...entries, entry];
const overflow = next.length - MAX_OFFLINE_QUEUE_ITEMS;
if (overflow <= 0) return { entries: next, accepted: true, evicted: 0 };
const dropIds = oldestTerminalIds(next, overflow);
const pruned = next.filter((e) => !dropIds.has(e.id));
if (pruned.length <= MAX_OFFLINE_QUEUE_ITEMS) {
return { entries: pruned, accepted: true, evicted: dropIds.size };
}
// No room even after pruning every terminal entry: the queue is full of
// actionable work. Reject without mutating anything.
return { entries, accepted: false, evicted: 0 };
}

export interface EnforceCapResult {
entries: OfflineQueueEntry[];
droppedTerminal: number;
/** Only nonzero for corrupt oversized state with no terminals to prune. */
droppedNewest: number;
}

/**
* Caps loaded (possibly corrupt/legacy oversized) state using the same rule:
* oldest terminal first; if actionable entries alone still exceed the cap,
* keep the oldest (mirrors `readVersionedCollection`) and report the drop.
*/
export function enforceQueueCap(entries: OfflineQueueEntry[]): EnforceCapResult {
const overflow = entries.length - MAX_OFFLINE_QUEUE_ITEMS;
if (overflow <= 0) return { entries, droppedTerminal: 0, droppedNewest: 0 };
const dropIds = oldestTerminalIds(entries, overflow);
const pruned = entries.filter((e) => !dropIds.has(e.id));
if (pruned.length <= MAX_OFFLINE_QUEUE_ITEMS) {
return { entries: pruned, droppedTerminal: dropIds.size, droppedNewest: 0 };
}
return {
entries: pruned.slice(0, MAX_OFFLINE_QUEUE_ITEMS),
droppedTerminal: dropIds.size,
droppedNewest: pruned.length - MAX_OFFLINE_QUEUE_ITEMS,
};
}
Loading
Loading