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
294 changes: 233 additions & 61 deletions src/Pages/Metrics/adminSwaps/TxSwaps.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,25 @@ import { useEffect, useMemo, useState } from "react"
import { Period } from "@/Components/Dropdowns/LVDropdown"
import { toast } from "react-toastify";
import * as Types from '../../../Api/pub/autogenerated/ts/types';
import { IonButton, IonContent, IonItem, IonPage, IonHeader, useIonLoading, useIonRouter, IonRow, IonText, IonInput } from "@ionic/react";
import { flashOutline, linkOutline, personOutline } from "ionicons/icons";
import { IonButton, IonContent, IonItem, IonPage, IonHeader, useIonLoading, useIonRouter, IonRow, IonText, IonInput, IonCard, IonCardContent, IonCardHeader, IonCardTitle, IonBadge, IonIcon } from "@ionic/react";
import { flashOutline, linkOutline, personOutline, walletOutline, checkmarkCircle, closeCircle } from "ionicons/icons";
import MetricsSubPageToolbar from "@/Layout2/Metrics/MetricsSubPageToolbar";
import { useAppSelector } from "@/State/store/hooks";
import { NprofileView, selectAdminNprofileViews } from "@/State/scoped/backups/sources/selectors";
import { selectSelectedMetricsAdminSourceId } from "@/State/runtime/slice";
import { TxSwapsList, TransactionSwapQuote } from "../../../Api/pub/autogenerated/ts/types";
import { TxSwapsList, TransactionSwapQuote, TxSwapOperation, UserOperationType } from "../../../Api/pub/autogenerated/ts/types";
import { fetcher, FetcherFuncs } from "../fetcher";
import AmountInput from "@/Components/AmountInput";



export default function TxSwaps({ adminSource }: { adminSource: NprofileView | undefined }) {
const router = useIonRouter();
const [amount, setAmount] = useState<string>("");
const [quotes, setQuotes] = useState<TransactionSwapQuote[]>([]);
const [address, setAddress] = useState<string>("");

const [swaps, setSwaps] = useState<TxSwapsList>({ quotes: [], swaps: [] });
const [swaps, setSwaps] = useState<TxSwapsList>({ swaps: [] });
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)

Expand All @@ -35,7 +37,7 @@ export default function TxSwaps({ adminSource }: { adminSource: NprofileView | u


useEffect(() => {
fetchSwaps()
fetchSwaps();
}, [adminSource?.sourceId])

const fetchSwaps = async () => {
Expand Down Expand Up @@ -67,11 +69,21 @@ export default function TxSwaps({ adminSource }: { adminSource: NprofileView | u
}


const oldQuotes = "quotes" in swaps && Array.isArray(swaps.quotes) ? swaps.quotes : [];
const completedSwaps = swaps.swaps ?? [];

return <IonContent className="ion-padding ion-content-no-footer">
{error && (
<div style={{ color: "red", padding: 12 }}>
<div style={{
padding: 12,
marginBottom: 12,
background: 'var(--ion-color-danger-tint)',
borderRadius: '8px',
color: 'var(--ion-color-danger-shade)',
border: '1px solid var(--ion-color-danger)',
}}>
<div style={{ fontWeight: 700, marginBottom: 8 }}>Something went wrong</div>
<div style={{ opacity: 0.85, marginBottom: 12 }}>{error}</div>
<div style={{ marginBottom: 12 }}>{error}</div>
<div style={{ display: "flex", gap: 8 }}>
<IonButton onClick={() => void fetchSwaps()}>Retry</IonButton>
<IonButton fill="outline" onClick={() => router.push("/metrics/select", "back")}>
Expand All @@ -80,60 +92,220 @@ export default function TxSwaps({ adminSource }: { adminSource: NprofileView | u
</div>
</div>
)}
{quotes.length === 0 && <>
<IonRow className="ion-margin-top"> <IonText>Amount received by destination address</IonText></IonRow>
<AmountInput
displayValue={amount}
effectiveSats={null}
onType={(text) => { setAmount(text); return text; }}
unit="sats"
onToggleUnit={() => { }}
/>
<IonButton onClick={requestQuote}>
Request Quotes
</IonButton>
</>}
{quotes.length > 0 && <IonRow><IonText className="ion-margin-top" style={{ fontSize: "1.2rem", fontWeight: "bold" }}>New Quotes</IonText></IonRow>}
{quotes.length > 0 && quotes.map(quote => <div key={quote.swap_operation_id}>
<IonRow> <IonText style={{ fontSize: "1.2rem", fontWeight: "bold" }}>Quote from {quote.service_url}</IonText></IonRow>
<IonRow className="ion-margin-top"> <IonText>{quote.transaction_amount_sats} sats will be sent to address</IonText></IonRow>
<IonRow > <IonText>{quote.swap_fee_sats + quote.chain_fee_sats} sats will be paid for the swap</IonText></IonRow>
<IonRow > <IonText>{quote.service_fee_sats} sats will be paid for the service</IonText></IonRow>
<IonRow className="ion-margin-top"> <IonText>{quote.service_fee_sats + quote.invoice_amount_sats} sats will be the total spent for the swap </IonText></IonRow>

<IonRow className="ion-margin-top"> <IonText>Address to send to</IonText></IonRow>
<IonInput
label="Address"
value={address}
onIonChange={(e) => setAddress(e.detail.value || "")}
/>
<IonButton onClick={() => doSwap(quote.swap_operation_id)}>Swap</IonButton>
</div>)}
{quotes.length > 0 && <IonButton onClick={cancelSwap}>Change Swap Amount</IonButton>}

{swaps.quotes.length > 0 && <IonRow><IonText className="ion-margin-top" style={{ fontSize: "1.2rem", fontWeight: "bold" }}>Old Quotes</IonText></IonRow>}
{swaps.quotes.map(quote => <div key={quote.swap_operation_id}>
<IonRow> <IonText style={{ fontSize: "1.2rem", fontWeight: "bold" }}>Quote from {quote.service_url}</IonText></IonRow>
<IonRow className="ion-margin-top"> <IonText>{quote.transaction_amount_sats} sats will be sent to address</IonText></IonRow>
<IonRow > <IonText>{quote.swap_fee_sats + quote.chain_fee_sats} sats will be paid for the swap</IonText></IonRow>
<IonRow > <IonText>{quote.service_fee_sats} sats will be paid for the service</IonText></IonRow>
<IonRow className="ion-margin-top"> <IonText>{quote.service_fee_sats + quote.invoice_amount_sats} sats will be the total spent for the swap </IonText></IonRow>

<IonRow className="ion-margin-top"> <IonText>Address to send to</IonText></IonRow>
<IonInput
label="Address"
value={address}
onIonChange={(e) => setAddress(e.detail.value || "")}
/>
<IonButton onClick={() => doSwap(quote.swap_operation_id)}>Swap</IonButton>
</div>)}
{swaps.swaps.length > 0 && <IonRow><IonText className="ion-margin-top" style={{ fontSize: "1.2rem", fontWeight: "bold" }}>Swaps</IonText></IonRow>}
{swaps.swaps.map(op => <div key={op.swap_operation_id}>
<IonItem>
{!op.failure_reason && <IonText>success: {op.operation_payment?.amount} sats to {op.address_paid}</IonText>}
{op.failure_reason && <IonText>failed: {op.operation_payment?.amount} sats to {op.address_paid} reason: {op.failure_reason}</IonText>}
</IonItem>
</div>)}
{quotes.length === 0 && (
<IonCard>
<IonCardHeader>
<IonCardTitle>Request New Transaction Swap Quote</IonCardTitle>
</IonCardHeader>
<IonCardContent>
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
<IonText color="medium">
Amount received by destination address (in sats)
</IonText>
<AmountInput
displayValue={amount}
effectiveSats={null}
onType={(text) => { setAmount(text); return text; }}
unit="sats"
onToggleUnit={() => { }}
/>
<IonButton
expand="block"
onClick={requestQuote}
disabled={!amount || Number(amount) <= 0}
>
<IonIcon icon={flashOutline} slot="start" />
Get Quotes from Services
</IonButton>
</div>
</IonCardContent>
</IonCard>
)}
{quotes.length > 0 && (
<>
<IonText className="ion-margin-top" style={{ fontSize: "1.4rem", fontWeight: "bold", display: "block", marginBottom: "12px" }}>
<IonIcon icon={flashOutline} style={{ marginRight: "8px" }} />
New Quotes Available
</IonText>
{quotes.map(quote => (
<IonCard key={quote.swap_operation_id} style={{ border: '2px solid var(--ion-color-primary)' }}>
<IonCardHeader>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<IonCardTitle style={{ fontSize: '1rem' }}>
{quote.service_url}
</IonCardTitle>
<IonBadge color="primary">New Quote</IonBadge>
</div>
</IonCardHeader>
<IonCardContent>
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
<div style={{
padding: '12px',
background: 'var(--ion-color-primary-tint)',
borderRadius: '8px',
marginBottom: '8px'
}}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '8px' }}>
<IonText><strong>Sent to address:</strong></IonText>
<IonText><strong>{quote.transaction_amount_sats.toLocaleString()} sats</strong></IonText>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '8px' }}>
<IonText color="warning"><strong>Swap + chain fee:</strong></IonText>
<IonText color="warning"><strong>{(quote.swap_fee_sats + quote.chain_fee_sats).toLocaleString()} sats</strong></IonText>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '8px' }}>
<IonText><strong>Service fee:</strong></IonText>
<IonText><strong>{quote.service_fee_sats.toLocaleString()} sats</strong></IonText>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<IonText><strong>Total spent:</strong></IonText>
<IonText><strong>{(quote.service_fee_sats + quote.invoice_amount_sats).toLocaleString()} sats</strong></IonText>
</div>
</div>
<div style={{ fontSize: '0.85rem' }}>
<div style={{ marginBottom: '4px' }}>
<IonText color="medium">Destination address to send to:</IonText>
</div>
<IonInput
label="Address"
value={address}
onIonChange={(e) => setAddress(e.detail.value || "")}
placeholder="Enter on-chain address"
style={{
marginTop: '4px',
background: 'var(--ion-color-light)',
borderRadius: '4px',
fontFamily: 'monospace',
fontSize: '0.85rem'
}}
/>
</div>
<IonButton
expand="block"
onClick={() => doSwap(quote.swap_operation_id)}
disabled={!address?.trim()}
>
<IonIcon icon={flashOutline} slot="start" />
Execute Swap
</IonButton>
</div>
</IonCardContent>
</IonCard>
))}
<IonButton
expand="block"
fill="outline"
onClick={cancelSwap}
style={{ marginTop: '8px', marginBottom: '16px' }}
>
Cancel & Request New Amount
</IonButton>
</>
)}

{/* Swap operations list */}
<IonCard>
<IonCardHeader>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<IonCardTitle>Swap Operations</IonCardTitle>
<IonButton size="small" fill="outline" onClick={() => void fetchSwaps()}>
Refresh
</IonButton>
</div>
</IonCardHeader>
<IonCardContent>
{(oldQuotes.length > 0 || completedSwaps.length > 0) && (
<IonText color="medium" style={{ fontSize: '0.9rem' }}>
{oldQuotes.length > 0 && <>Pending quotes: {oldQuotes.length}</>}
{oldQuotes.length > 0 && completedSwaps.length > 0 && " · "}
{completedSwaps.length > 0 && <>Completed: {completedSwaps.length}</>}
</IonText>
)}
</IonCardContent>
</IonCard>

{oldQuotes.length === 0 && completedSwaps.length === 0 && quotes.length === 0 && (
<IonCard>
<IonCardContent style={{ textAlign: 'center', padding: '32px' }}>
<IonIcon
icon={walletOutline}
style={{ fontSize: '64px', color: 'var(--ion-color-medium)', marginBottom: '16px' }}
/>
<IonText color="medium">
<h3>No swap operations yet</h3>
<p>Request a quote above to get started with transaction swaps</p>
</IonText>
</IonCardContent>
</IonCard>
)}

{oldQuotes.length > 0 && (
<>
<IonText className="ion-margin-top" style={{ fontSize: "1.2rem", fontWeight: "bold", display: "block", marginBottom: "8px" }}>
Pending Quotes
</IonText>
{oldQuotes.map(quote => (
<IonCard key={quote.swap_operation_id}>
<IonCardHeader>
<IonCardTitle style={{ fontSize: '1rem' }}>{quote.service_url}</IonCardTitle>
</IonCardHeader>
<IonCardContent>
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<IonText color="medium">Sent to address:</IonText>
<IonText><strong>{quote.transaction_amount_sats.toLocaleString()} sats</strong></IonText>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<IonText color="medium">Swap + chain fee:</IonText>
<IonText><strong>{(quote.swap_fee_sats + quote.chain_fee_sats).toLocaleString()} sats</strong></IonText>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<IonText color="medium">Total spent:</IonText>
<IonText><strong>{(quote.service_fee_sats + quote.invoice_amount_sats).toLocaleString()} sats</strong></IonText>
</div>
<IonInput
label="Address"
value={address}
onIonChange={(e) => setAddress(e.detail.value || "")}
placeholder="Enter on-chain address"
style={{ marginTop: '8px' }}
/>
<IonButton size="small" onClick={() => doSwap(quote.swap_operation_id)} disabled={!address?.trim()}>
Execute Swap
</IonButton>
</div>
</IonCardContent>
</IonCard>
))}
</>
)}

{completedSwaps.length > 0 && (
<>
<IonText className="ion-margin-top" style={{ fontSize: "1.2rem", fontWeight: "bold", display: "block", marginBottom: "8px" }}>
Completed Swaps
</IonText>
{completedSwaps.map(op => (
<IonCard key={op.quote.swap_operation_id}>
<IonCardContent>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', flexWrap: 'wrap' }}>
{!op.failure_reason ? (
<>
<IonIcon icon={checkmarkCircle} color="success" style={{ fontSize: '24px' }} />
<IonText>Success: {op.operation_payment?.amount?.toLocaleString()} sats to {op.address_paid ?? '—'}</IonText>
</>
) : (
<>
<IonIcon icon={closeCircle} color="danger" style={{ fontSize: '24px' }} />
<IonText color="danger">Failed: {op.operation_payment?.amount?.toLocaleString()} sats to {op.address_paid ?? '—'} — {op.failure_reason}</IonText>
</>
)}
</div>
</IonCardContent>
</IonCard>
))}
</>
)}
</IonContent>

}
Loading
Loading