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
4 changes: 2 additions & 2 deletions apps/app/components/Settings/NetworkRpcEditView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { validateRpcUrl } from "../../lib/validators/rpcValidator"
import SecondaryButton from "../buttons/secondaryButton"
import SubmitButton from "../buttons/submitButton"
import { toast } from "react-hot-toast"
import LightClient from "../../lib/lightClient"
import { supportsLightClient } from "../../lib/lightClient/supportsNetwork"
import Image from 'next/image'

interface NetworkRpcEditViewProps {
Expand All @@ -21,7 +21,7 @@ const NetworkRpcEditView: FC<NetworkRpcEditViewProps> = ({ network, onSave }) =>
const [validationErrors, setValidationErrors] = useState<Record<number, string>>({})
const [validatedUrls, setValidatedUrls] = useState<Record<number, boolean>>({})

const hasLightClient = new LightClient().supportsNetwork(network)
const hasLightClient = supportsLightClient(network)

useEffect(() => {
// Load existing URLs or start with one empty field
Expand Down
8 changes: 2 additions & 6 deletions apps/app/components/Settings/RpcNetworkListView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Settings2, Search, Zap } from "lucide-react"
import { useSettingsState } from "../../context/settings"
import { Network } from "../../Models/Network"
import { useRpcConfigStore } from "../../stores/rpcConfigStore"
import LightClient from "../../lib/lightClient"
import { supportsLightClient } from "../../lib/lightClient/supportsNetwork"
import Image from 'next/image'

interface RpcNetworkListViewProps {
Expand All @@ -15,10 +15,6 @@ const RpcNetworkListView: FC<RpcNetworkListViewProps> = ({ onNetworkSelect }) =>
const { rpcConfigs, isUsingCustomRpc } = useRpcConfigStore()
const [searchQuery, setSearchQuery] = useState<string>('')

const hasLightClient = (network: Network): boolean => {
return new LightClient().supportsNetwork(network)
}

// Filter for all networks with RPC URLs
const networksWithRpc = settings?.networks?.filter(
network => network.nodes?.[0]?.url && network.nodes[0].url !== ""
Expand Down Expand Up @@ -84,7 +80,7 @@ const RpcNetworkListView: FC<RpcNetworkListViewProps> = ({ onNetworkSelect }) =>
<span className="font-medium text-primary-text">
{network.displayName}
</span>
{hasLightClient(network) && (
{supportsLightClient(network) && (
<span className="flex items-center gap-1 px-1.5 py-0.5 text-xs font-medium bg-blue-900/20 text-blue-400 rounded">
<Zap className="w-3 h-3" />
Light Client
Expand Down
4 changes: 2 additions & 2 deletions apps/app/components/Swap/Atomic/Form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ const SwapForm: FC<SwapFormProps> = ({ polling = true, onQuoteChange }) => {
}, [quote, solverId, onQuoteChange])

const actionDisplayName = query?.buttonTextColor || "Swap now"
const shouldConnectWallet = !wallets.length;
const shouldConnectDestinationWallet = !hasRequiredDestinationWallet(destination, providers);
const shouldConnectWallet = values.from && !wallets.length;
const shouldConnectDestinationWallet = values.to && !hasRequiredDestinationWallet(destination, providers);

return <>
<Form className={`h-full space-y-2 ${(isSubmitting) ? 'pointer-events-none' : 'pointer-events-auto'}`} >
Expand Down
6 changes: 5 additions & 1 deletion apps/app/components/Swap/AtomicChat/Actions/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,9 @@ const SolverLockDetectedAction: FC<{ type: SwapViewType }> = ({ type }) => {
const [autoRevealFailed, setAutoRevealFailed] = useState(false)
const attemptedRef = useRef(false)
const { verified, skipped, mismatches } = useSolverLockVerification()
const { lightClientPending } = useAtomicState()

const shouldAutoReveal = autoRevealSecret && hasSeenAutoRevealPrompt && !autoRevealFailed && verified
const shouldAutoReveal = autoRevealSecret && hasSeenAutoRevealPrompt && !autoRevealFailed && verified && !lightClientPending

useEffect(() => {
if (shouldAutoReveal && !attemptedRef.current) {
Expand All @@ -100,6 +101,9 @@ const SolverLockDetectedAction: FC<{ type: SwapViewType }> = ({ type }) => {
}
}, [shouldAutoReveal, revealSecret])

// Wait for light client verification before allowing secret reveal
if (lightClientPending) return <></>

if (shouldAutoReveal) return <></>

// Verification failed — hide reveal button, progress panel shows the error
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ export function useSwapProgress(): SwapProgress {
subtitle: "You will receive your assets shortly.",
steps: buildSteps(HAPPY_STEPS, 3, { source: sourceTxLink, dest: destTxLink }, {
0: { timelock: sourceDetails?.timelock },
1: { description: <VerificationStatus /> },
3: { name: "Receiving assets", status: StepStatus.Current, description: "Solver is claiming on destination" },
}),
};
Expand All @@ -234,6 +235,7 @@ export function useSwapProgress(): SwapProgress {
title: "Action required",
subtitle: "Claim your assets manually on the destination chain.",
steps: buildSteps(HAPPY_STEPS, 3, { source: sourceTxLink, dest: destTxLink }, {
1: { description: <VerificationStatus /> },
3: { name: "Claim assets", status: !redeemTxLink ? StepStatus.Upcoming : StepStatus.Current, description: "Solver didn't complete the claim. You can claim your assets manually." },
}),
};
Expand All @@ -245,7 +247,9 @@ export function useSwapProgress(): SwapProgress {
gaugeValue: 100, gaugeIcon: "check",
title: "Swap complete",
subtitle: "Your assets have been sent to your address.",
steps: buildSteps(HAPPY_STEPS, -1, { redeem: redeemTxLink, source: sourceTxLink, dest: destTxLink }),
steps: buildSteps(HAPPY_STEPS, -1, { redeem: redeemTxLink, source: sourceTxLink, dest: destTxLink }, {
1: { description: <VerificationStatus /> },
}),
};
}

Expand Down
87 changes: 29 additions & 58 deletions apps/app/context/atomicContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { useSettingsState } from './settings';
import { LockDetails, LockStatus } from '../Models/phtlc/PHTLC';
import { Network, Token } from '@/Models/Network';
import { HTLCFromApi, HTLCTransaction, resolveHTLCStatus, IHTLCClient } from '@train-protocol/sdk';
import LightClient from '@/lib/lightClient';
import { SwapData, useSwapStore } from '@/stores/swapStore';
import { useShallow } from 'zustand/react/shallow';
import { resolvePersistantQueryParams } from '@/helpers/querryHelper';
Expand All @@ -17,6 +16,8 @@ import { useSelectedAccount } from './swapAccounts';
import useWallet from '@/hooks/useWallet';
import { Address } from '@/lib/address';
import { useRpcConfigStore } from '@/stores/rpcConfigStore';
import { useLightClient } from '@/hooks/htlc/useLightClient'
import { supportsLightClient } from '@/lib/lightClient/supportsNetwork';

const AtomicStateContext = createContext<DataContextType | null>(null);

Expand All @@ -32,25 +33,24 @@ type DataContextType = HTLCState & {
htlcStatus: HTLCStatus,
destRedeemTx?: string,
verifyingByLightClient: boolean,
lightClientPending: boolean,
destinationDetailsByLightClient?: { data?: LockDetails, error?: string },
srcAtomicContract?: string,
destAtomicContract?: string,
sourceClient?: IHTLCClient,
destinationClient?: IHTLCClient,
error?: { message: string, buttonText?: string },
setError: (error: { message: string, buttonText?: string } | undefined) => void;
setManualClaimTxId: (txId: string | undefined) => void;
setVerifyingByLightClient: (value: boolean) => void;
onUserLock: (hashlock: string, txId: string) => void;
updateHTLC: (field: keyof HTLCState, value: any) => void;
}

interface HTLCState {
sourceDetails?: LockDetails;
solverLockDetails?: LockDetails;
destinationDetailsByLightClient?: { data?: LockDetails, error?: string };
secretRevealed?: boolean;
htlcFromApi?: HTLCFromApi;
lightClient?: LightClient | undefined;
isTimelockExpired: boolean;
manualClaimRequired?: boolean;
refundTxId?: string | null;
Expand Down Expand Up @@ -104,8 +104,6 @@ export function AtomicProvider({ children }) {
const [htlcStates, setHtlcStates] = useState<CommitStatesDict>({});
const [error, setError] = useState<{ message: string, buttonText?: string } | undefined>(undefined);
const [manualClaimTxId, setManualClaimTxId] = useState<string | undefined>(undefined);
const [lightClient, setLightClient] = useState<LightClient | undefined>(undefined);
const [verifyingByLightClient, setVerifyingByLightClient] = useState(false)

const [sourceClient, setSourceClient] = useState<IHTLCClient | undefined>(undefined);
const [destinationClient, setDestinationClient] = useState<IHTLCClient | undefined>(undefined);
Expand Down Expand Up @@ -150,7 +148,6 @@ export function AtomicProvider({ children }) {
const htlcFromApi = hashlock ? htlcStates[hashlock]?.htlcFromApi : undefined;
const isTimelockExpired = hashlock ? htlcStates[hashlock]?.isTimelockExpired : false;
const manualClaimRequired = hashlock ? htlcStates[hashlock]?.manualClaimRequired : false;
const destinationDetailsByLightClient = hashlock ? htlcStates[hashlock]?.destinationDetailsByLightClient : undefined

const destinationRedeemTx = manualClaimTxId ?? htlcFromApi?.transactions?.find(t => t.type === HTLCTransaction.HTLCRedeem && t.networkId === destination)?.hash

Expand All @@ -170,10 +167,18 @@ export function AtomicProvider({ children }) {

const htlcStatus = useMemo(() =>
resolveHTLCStatus({ sourceDetails, solverLockDetails, timelockExpired: isTimelockExpired, secretRevealed, manualClaimRequired, destRedeemTxId: destinationRedeemTx }),
[sourceDetails, solverLockDetails, isTimelockExpired, secretRevealed, manualClaimRequired])
[sourceDetails, solverLockDetails, isTimelockExpired, secretRevealed, manualClaimRequired, destinationRedeemTx])

const isTerminal = isTerminalStatus(htlcStatus)

const { lightClientInitialized, lightClientPending, verifyingByLightClient, destinationDetailsByLightClient } = useLightClient({
destination_network,
destination_token,
hashlock,
destAtomicContract,
isTerminal,
})

useEffect(() => {
if (!activeHashlock) return
const currentSwapData = useSwapStore.getState().swaps[activeHashlock]
Expand Down Expand Up @@ -252,6 +257,18 @@ export function AtomicProvider({ children }) {
onSuccess: handleUserLockSuccess,
})

const solverPollNodeUrls = useMemo(() => {
if (!destination_network) return []
const allUrls = getEffectiveRpcUrls(destination_network)
const useSingleRpc = supportsLightClient(destination_network)
&& lightClientInitialized
&& !destinationDetailsByLightClient?.error
if (useSingleRpc && allUrls.length > 0) {
return allUrls.slice(0, 1)
}
return allUrls
}, [destination_network, getEffectiveRpcUrls, lightClientInitialized, destinationDetailsByLightClient?.error])

useSolverLockPolling({
network: destination_network,
hashlock,
Expand All @@ -261,54 +278,9 @@ export function AtomicProvider({ children }) {
client: destinationClient,
solverAddress: destinationSolverAddress,
onSuccess: handleSolverLockSuccess,
nodeUrls: destination_network ? getEffectiveRpcUrls(destination_network) : [],
nodeUrls: solverPollNodeUrls,
})

// useEffect(() => {
// if (destination_network && htlcStatus !== HTLCStatus.TimelockExpired && htlcStatus !== HTLCStatus.RedeemCompleted) {
// (async () => {
// try {
// const lightClient = new LightClient()
// await lightClient.initProvider({ network: destination_network })
// setLightClient(lightClient)
// } catch (error) {
// console.log(error)
// }

// })()
// }
// }, [destination_network])

// useEffect(() => {
// (async () => {
// if (destination_network && destination_token && hashlock && destination_asset && lightClient && !sourceDetails?.hashlock && destAtomicContract) {
// if (!lightClient.supportsNetwork(destination_network)) return

// try {
// setVerifyingByLightClient(true)
// const data = await lightClient.getDetails({
// network: destination_network,
// token: destination_token,
// hashlock,
// atomicContract: destAtomicContract
// })
// if (data) {
// updateCommit('destinationDetailsByLightClient', { data })
// return
// }
// }
// catch (e) {
// updateCommit('destinationDetailsByLightClient', { data: undefined, error: 'Light client is not available' })
// console.log(e)
// }
// finally {
// setVerifyingByLightClient(false)
// }
// }
// })()
// }, [destination_network, hashlock, destAtomicContract, lightClient, destination_token, sourceDetails, destination_asset])


useEffect(() => {
let timer: ReturnType<typeof setTimeout>;

Expand Down Expand Up @@ -349,7 +321,7 @@ export function AtomicProvider({ children }) {
return () => clearTimeout(timer);
}, [sourceDetails?.status, sourceDetails?.secret, solverLockDetails?.sender, solverLockDetails?.status, hashlock, manualClaimRequired])

const handleCommited = (hashlock: string, txId: string) => {
const onUserLock = (hashlock: string, txId: string) => {
// Move tempSwap → swaps[hashlock] in the store (also sets activeHashlock)
commitSwap(hashlock, txId)

Expand All @@ -370,7 +342,7 @@ export function AtomicProvider({ children }) {
return (
<AtomicStateContext.Provider value={{
source_network,
onUserLock: handleCommited,
onUserLock,
source_asset: source_token,
destination_asset: destination_token,
address: address as string,
Expand All @@ -386,18 +358,17 @@ export function AtomicProvider({ children }) {
setError,
setManualClaimTxId,
htlcFromApi: htlcFromApi,
lightClient,
htlcStatus,
isTimelockExpired,
refundTxId,
destRedeemTx: destinationRedeemTx,
verifyingByLightClient,
lightClientPending,
destinationDetailsByLightClient,
srcAtomicContract,
destAtomicContract,
sourceClient,
destinationClient,
setVerifyingByLightClient,
updateHTLC: updateCommit,
}}>
{children}
Expand Down
Loading