diff --git a/context/secretDerivationContext.tsx b/context/secretDerivationContext.tsx
new file mode 100644
index 00000000..e5f21051
--- /dev/null
+++ b/context/secretDerivationContext.tsx
@@ -0,0 +1,207 @@
+// context/secretDerivationContext.tsx
+
+import { createContext, useContext, useEffect, useCallback, ReactNode } from 'react';
+import { Wallet } from '@/Models/WalletProvider';
+import {
+ DerivationMethod,
+ checkPrfSupport,
+ deriveKeyWithPasskey,
+ registerPasskey,
+ deriveSecretFromTimelock
+} from '@/lib/htlc/secretDerivation';
+import { deriveKeyFromEvmSignature } from '@/lib/htlc/secretDerivation/walletSign/evm';
+import { useSecretDerivationStore, DerivationStatus } from '@/stores/secretDerivationStore';
+
+interface SecretDerivationContextValue {
+ method: DerivationMethod | null;
+ isLoggedIn: boolean;
+ loginWallet: Wallet | null;
+ loginWithPasskey: (options?: { createIfMissing?: boolean }) => Promise;
+ loginWithNewPasskey: (label?: string) => Promise;
+ loginWithWallet: (config: any, wallet: Wallet) => Promise;
+ logout: () => void;
+ isPasskeySupported: boolean;
+ deriveInitialKey: (params: DeriveKeyParams) => Promise;
+ deriveSecret: (params: DeriveSecretParams) => Promise;
+ isReady: boolean;
+ /** Set while user is completing passkey or wallet sign */
+ derivationStatus: DerivationStatus;
+ /** Message to show during signing, e.g. "Confirm with passkey" or "Please sign in your wallet" */
+ derivationMessage: string;
+}
+
+interface DeriveKeyParams {
+ chainId: string | number;
+ wallet?: Wallet;
+ config?: any; // Wagmi config for EVM
+ tonConnectUI?: any; // TON Connect UI
+}
+
+interface DeriveSecretParams extends DeriveKeyParams {
+ timelock: number;
+}
+
+const SecretDerivationContext = createContext(undefined);
+
+interface SecretDerivationProviderProps {
+ children: ReactNode;
+}
+
+export function SecretDerivationProvider({ children }: SecretDerivationProviderProps) {
+ // Get all state from the zustand store
+ const {
+ method,
+ isPasskeySupported,
+ isReady,
+ derivationStatus,
+ derivationMessage,
+ isLoggedIn,
+ loginWallet,
+ storedDerivedKey,
+ logout,
+ } = useSecretDerivationStore();
+
+ // Get setState function for updating store
+ const setState = useSecretDerivationStore.setState;
+
+ // Check passkey support on mount (localStorage restore is handled by zustand persist)
+ useEffect(() => {
+ checkPrfSupport().then((supported) => {
+ setState({ isPasskeySupported: supported, isReady: true });
+ });
+ }, [setState]);
+
+ const loginWithPasskey = useCallback(async (options?: { createIfMissing?: boolean }) => {
+ setState({ derivationStatus: 'signing', derivationMessage: 'Confirm with your passkey' });
+ try {
+ const { key, credentialId } = await deriveKeyWithPasskey(options);
+ setState({
+ method: 'passkey',
+ isLoggedIn: true,
+ loginWallet: null,
+ storedDerivedKey: key,
+ passkeyCredentialId: credentialId,
+ });
+ } finally {
+ setState({ derivationStatus: 'idle', derivationMessage: '' });
+ }
+ }, [setState]);
+
+ const loginWithNewPasskey = useCallback(async (label?: string) => {
+ setState({ derivationStatus: 'signing', derivationMessage: 'Confirm with your passkey' });
+ try {
+ await registerPasskey(true, label);
+ const { key, credentialId } = await deriveKeyWithPasskey();
+ setState({
+ method: 'passkey',
+ isLoggedIn: true,
+ loginWallet: null,
+ storedDerivedKey: key,
+ passkeyCredentialId: credentialId,
+ });
+ } finally {
+ setState({ derivationStatus: 'idle', derivationMessage: '' });
+ }
+ }, [setState]);
+
+ const loginWithWallet = useCallback(async (config: any, wallet: Wallet) => {
+ if (wallet.providerName?.toLowerCase() !== 'evm') {
+ throw new Error('Only EVM wallets are supported for login right now');
+ }
+ setState({ derivationStatus: 'signing', derivationMessage: 'Please sign in your wallet' });
+ try {
+ const derivedKey = await deriveKeyFromEvmSignature(config, wallet.address as `0x${string}`);
+ setState({
+ method: 'wallet_sign',
+ isLoggedIn: true,
+ loginWallet: wallet,
+ storedDerivedKey: derivedKey,
+ });
+ } finally {
+ setState({ derivationStatus: 'idle', derivationMessage: '' });
+ }
+ }, [setState]);
+
+ const deriveInitialKey = useCallback(async (params: DeriveKeyParams): Promise => {
+ const { wallet, config } = params;
+
+ if (!method) {
+ throw new Error('No derivation method selected. Please choose passkey or wallet sign.');
+ }
+
+ // If we have a stored key from login, return it directly
+ if (storedDerivedKey) {
+ return storedDerivedKey;
+ }
+
+ // Fallback: Re-authenticate if no stored key
+ if (method === 'passkey') {
+ const { key, credentialId } = await deriveKeyWithPasskey();
+ setState({ storedDerivedKey: key, passkeyCredentialId: credentialId });
+ return key;
+ }
+
+ // Wallet sign method
+ if (!wallet) {
+ throw new Error('Wallet required for wallet_sign method');
+ }
+
+ const providerName = wallet.providerName?.toLowerCase();
+
+ if (providerName === 'evm') {
+ if (!config) {
+ throw new Error('Wagmi config required for EVM wallets');
+ }
+ return await deriveKeyFromEvmSignature(config, wallet.address as `0x${string}`);
+ }
+
+ throw new Error(`Unsupported provider: ${providerName}`);
+ }, [method, storedDerivedKey, setState]);
+
+ const deriveSecret = useCallback(async (params: DeriveSecretParams): Promise => {
+ setState({
+ derivationStatus: 'signing',
+ derivationMessage: method === 'passkey'
+ ? 'Confirm with your passkey'
+ : 'Please sign in your wallet'
+ });
+ try {
+ const { timelock, ...keyParams } = params;
+ const initialKey = await deriveInitialKey(keyParams);
+ const derivedKey = deriveSecretFromTimelock(initialKey, timelock);
+ return '0x' + derivedKey.toString('hex');
+ } finally {
+ setState({ derivationStatus: 'idle', derivationMessage: '' });
+ }
+ }, [deriveInitialKey, method, setState]);
+
+ const value: SecretDerivationContextValue = {
+ method,
+ isLoggedIn,
+ loginWallet,
+ loginWithPasskey,
+ loginWithNewPasskey,
+ loginWithWallet,
+ logout,
+ isPasskeySupported,
+ deriveInitialKey,
+ deriveSecret,
+ isReady,
+ derivationStatus,
+ derivationMessage,
+ };
+
+ return (
+
+ {children}
+
+ );
+}
+
+export function useSecretDerivation() {
+ const context = useContext(SecretDerivationContext);
+ if (!context) {
+ throw new Error('useSecretDerivation must be used within SecretDerivationProvider');
+ }
+ return context;
+}
diff --git a/hooks/useSteps.ts b/hooks/useSteps.ts
new file mode 100644
index 00000000..4742e68b
--- /dev/null
+++ b/hooks/useSteps.ts
@@ -0,0 +1,69 @@
+import { useState, useCallback, useMemo } from 'react';
+
+type Direction = 'forward' | 'back';
+
+interface StepConfig {
+ initial: T;
+}
+
+interface UseStepsReturn {
+ currentStep: T;
+ direction: Direction;
+ canGoBack: boolean;
+ goToStep: (step: T, direction?: Direction) => void;
+ goBack: () => void;
+ reset: () => void;
+ isStep: (step: T) => boolean;
+}
+
+function useSteps(config: StepConfig): UseStepsReturn {
+ const { initial } = config;
+
+ const [currentStep, setCurrentStep] = useState(initial);
+ const [direction, setDirection] = useState('forward');
+ const [stepHistory, setStepHistory] = useState([initial]);
+
+ const canGoBack = useMemo(() => stepHistory.length > 1, [stepHistory]);
+
+ const goToStep = useCallback((step: T, dir?: Direction) => {
+ const newDirection = dir ?? 'forward';
+ setDirection(newDirection);
+ setCurrentStep(step);
+
+ if (newDirection === 'forward') {
+ setStepHistory(prev => [...prev, step]);
+ }
+ }, []);
+
+ const goBack = useCallback(() => {
+ if (stepHistory.length <= 1) return;
+
+ setDirection('back');
+ setStepHistory(prev => {
+ const newHistory = prev.slice(0, -1);
+ const previousStep = newHistory[newHistory.length - 1];
+ setCurrentStep(previousStep);
+ return newHistory;
+ });
+ }, [stepHistory.length]);
+
+ const reset = useCallback(() => {
+ setCurrentStep(initial);
+ setDirection('forward');
+ setStepHistory([initial]);
+ }, [initial]);
+
+ const isStep = useCallback((step: T) => currentStep === step, [currentStep]);
+
+ return {
+ currentStep,
+ direction,
+ canGoBack,
+ goToStep,
+ goBack,
+ reset,
+ isStep,
+ };
+}
+
+export { useSteps };
diff --git a/lib/htlc/secretDerivation/FLOW.md b/lib/htlc/secretDerivation/FLOW.md
new file mode 100644
index 00000000..439c0c4a
--- /dev/null
+++ b/lib/htlc/secretDerivation/FLOW.md
@@ -0,0 +1,187 @@
+# Secret derivation flow
+
+End-to-end path from UI to HTLC secret/hashlock.
+
+---
+
+## 1. App bootstrap (provider tree)
+
+```
+components/WalletProviders/index.tsx
+```
+
+- **SecretDerivationProvider** wraps the whole tree (outermost).
+- Under it: TonConnect → Solana → **StarknetProvider** → EvmConnectors → Wagmi → … → **WalletProvidersProvider** → app.
+
+So any component under this tree can call `useSecretDerivation()`.
+
+---
+
+## 2. Unified login modal
+
+```
+components/SecretDerivation/LoginModal.tsx
+```
+
+- **LoginModal** handles the entire login flow in a single modal:
+ - pick method → wallet select/connect (EVM only) → signing.
+- Method is **not persisted**; login is session-only.
+
+**Where it opens:**
+
+### Primary: First page (FormButton - before "Swap now")
+**`components/Swap/FormButton.tsx`** - The main swap form button checks `isLoggedIn`:
+- If **not logged in** → shows **"Login to continue"** button.
+- Clicking opens **LoginModal**.
+- After successful login → **"Swap now"** button appears.
+
+**This enforces that users MUST login before they can swap.**
+
+---
+
+## 3. User triggers "Commit" (HTLC create)
+
+```
+components/Swap/AtomicChat/Actions/UserActions.tsx → UserCommitAction
+```
+
+- User clicks commit.
+- `handleCommit()` runs.
+- `provider` comes from `useWallet(source_network, 'withdrawal')` (the wallet provider for the source chain).
+- It calls:
+
+ ```ts
+ provider.createPreHTLC({ address, amount, destinationChain, sourceChain, ... })
+ ```
+
+- `provider` is one of the objects returned by `useStarknet` / `useEVM` / `useSVM` / `useTON` / `useFuel` / `useAztec` (see step 4).
+
+---
+
+## 4. Wallet provider → atomic hook → createPreHTLC
+
+Each chain has a "useX" hook that builds the `WalletProvider` and uses an atomic hook:
+
+| Chain | Wallet hook | Atomic hook | File |
+|--------|------------------------|------------------------|---------------------------|
+| Starknet | `useStarknet` | `useAtomicStarknet` | `lib/wallets/starknet/useStarknet.ts` → `useAtomicStarknet.ts` |
+| EVM | `useEVM` | `useAtomicEVM` | `lib/wallets/evm/useEVM.ts` → `useAtomicEVM.ts` |
+| Solana | `useSVM` | `useAtomicSVM` | `lib/wallets/solana/useAtomicSVM.ts` |
+| TON | `useTON` | `useAtomicTON` | `lib/wallets/ton/useAtomicTON.ts` |
+| Fuel | `useFuel` | `useAtomicFuel` | `lib/wallets/fuel/useAtomicFuel.ts` |
+| Aztec | `useAztec` | `useAtomicAztec` | `lib/wallets/aztec/useAtomicAztec.ts` |
+
+Example (Starknet):
+
+- `context/walletHookProviders.tsx` → `useStarknet()` → returns `provider` with `createPreHTLC` and other methods.
+- `useStarknet` (e.g. `lib/wallets/starknet/useStarknet.ts` line 131) calls `useAtomicStarknet({ starknetWallet, nodeUrl })` and spreads `atomicFunctions` (including `createPreHTLC`) onto `provider`.
+
+So the flow is: **UserActions** → `provider.createPreHTLC(...)` → **atomic hook's** `createPreHTLC` (e.g. `useAtomicStarknet`).
+
+---
+
+## 5. createPreHTLC → deriveSecret (per chain)
+
+In each atomic file, `createPreHTLC`:
+
+1. Calls **useSecretDerivation()** and gets **deriveSecret**.
+2. Calls **deriveSecret({ chainId, wallet, timelock })** (timelock from `calculateEpochTimelock(40)` or similar).
+3. Builds `secret` buffer and **hashlock = sha256(secret)**.
+4. Builds the chain-specific transaction (Starknet call, EVM tx, Solana tx, etc.). **Hashlock is not yet passed to the contract** in many places — see "Note: Add hashlock to args…" in each file.
+
+Files:
+
+- `lib/wallets/starknet/useAtomicStarknet.ts` (around 66)
+- `lib/wallets/evm/useAtomicEVM.ts` (around 76)
+- `lib/wallets/solana/useAtomicSVM.ts` (around 52)
+- `lib/wallets/ton/useAtomicTON.ts` (around 39)
+- `lib/wallets/fuel/useAtomicFuel.ts` (around 55)
+- `lib/wallets/aztec/useAtomicAztec.ts` (around 38)
+
+---
+
+## 6. deriveSecret implementation (context)
+
+```
+context/secretDerivationContext.tsx
+```
+
+- **deriveSecret(params)**:
+ 1. Reads **method** from context (passkey vs wallet_sign).
+ 2. If no method → throws "No derivation method selected".
+ 3. **deriveInitialKey(params)**:
+ - Returns stored key from login if available.
+ - Fallback re-authentication:
+ - **passkey** → `deriveKeyWithPasskey()` in `lib/htlc/secretDerivation/passkeyService.ts` (WebAuthn PRF with fixed identity salt).
+ - **wallet_sign** → `deriveKeyFromEvmSignature()` (EIP-712 signature with fixed identity salt).
+ 4. **deriveSecretFromTimelock(initialKey, timelock)** → `lib/htlc/secretDerivation/keyDerivation.ts` (HKDF with timelock salt).
+ 5. Returns secret as hex string.
+
+**Note:** The initial key derivation uses a fixed identity salt (`train-identity-v1`) rather than chain-specific salts. Secret uniqueness comes from the timelock parameter at commit time.
+
+---
+
+## 7. Core crypto (key derivation)
+
+```
+lib/htlc/secretDerivation/
+```
+
+- **keyDerivation.ts**
+ - `deriveKeyMaterial(ikm, salt)` — HKDF(sha2).
+ - `deriveSecretFromTimelock(initialKey, timelock)` — HKDF with timelock salt; returns 32-byte secret.
+ - `normalizeHex`, etc.
+
+- **passkeyService.ts**
+ - WebAuthn PRF: `deriveKeyWithPasskey()` → 32-byte key using fixed identity salt.
+ - `checkPrfSupport()`, `registerPasskey()`, stored credential ID.
+
+- **walletSign/evm.ts**
+ - EIP-712 typed data signature → `deriveKeyMaterial(signature, identitySalt)` → 32-byte key.
+
+---
+
+## Flow diagram (summary)
+
+```
+[App]
+ SecretDerivationProvider
+ → FormButton: if not logged in → "Login to continue" (opens LoginModal)
+ → User completes login (passkey or EVM wallet) → "Swap now" appears
+ → Login derives initialKey using fixed identity salt, stored in context
+
+[User clicks Swap now → goes to page 2]
+
+[User clicks Commit]
+ UserActions.handleCommit()
+ → provider.createPreHTLC(...)
+ → useAtomicEVM / … createPreHTLC()
+ → useSecretDerivation().deriveSecret({ timelock, ... })
+ → SecretDerivationContext.deriveSecret()
+ → deriveInitialKey() → returns stored key from login
+ → deriveSecretFromTimelock(initialKey, timelock) → HKDF with timelock
+ → secret → sha256 → hashlock
+ → build tx with hashlock
+```
+
+---
+
+## UI/UX Flow Summary
+
+1. **First page (swap form):** User must click "Login to continue" → LoginModal handles method selection + EVM wallet selection/connection → "Swap now" button appears.
+2. **Form submission guard:** When user clicks "Swap now", `handleSubmit` checks `isLoggedIn`:
+ - If **not logged in** → Shows error "Please login first" and prevents navigation to page 2.
+ - If **logged in** → Proceeds to page 2 (commit/atomic flow).
+3. **Second page guard:** `AtomicChat` component checks on mount:
+ - If **not logged in** → Automatically redirects back to page 1 with error toast.
+ - If **logged in** → Renders the commit page normally.
+4. **Commit action:** User clicks "Confirm in wallet" → SignFlowModal shows signing state while the wallet/passkey signature happens.
+5. **Method is not persisted**; user logs in each session when needed.
+
+## Security Layers
+
+The implementation has **three layers of protection** to ensure users cannot access page 2 without logging in:
+
+1. **UI Layer:** "Login to continue" button instead of "Swap now" (in `FormButton.tsx`)
+2. **Submit Layer:** Form submission validation blocks navigation (in `Atomic/index.tsx` `handleSubmit`)
+3. **Page Guard:** Page 2 redirects back if accessed without login (in `AtomicChat/index.tsx`)
diff --git a/lib/htlc/secretDerivation/index.ts b/lib/htlc/secretDerivation/index.ts
new file mode 100644
index 00000000..155fc088
--- /dev/null
+++ b/lib/htlc/secretDerivation/index.ts
@@ -0,0 +1,6 @@
+// lib/htlc/secretDerivation/index.ts
+
+export * from './types';
+export * from './keyDerivation';
+export * from './passkeyService';
+export * from './walletSign';
diff --git a/lib/htlc/secretDerivation/keyDerivation.ts b/lib/htlc/secretDerivation/keyDerivation.ts
new file mode 100644
index 00000000..b5af5fbb
--- /dev/null
+++ b/lib/htlc/secretDerivation/keyDerivation.ts
@@ -0,0 +1,45 @@
+// lib/htlc/secretDerivation/keyDerivation.ts
+
+import { hkdf } from "@noble/hashes/hkdf.js";
+import { sha256 } from "@noble/hashes/sha2.js";
+
+const HKDF_INFO = Buffer.from('train-signature-key-derivation', 'utf8');
+const KEY_LENGTH = 32; // 256 bits
+
+// Normalize hex string to even length
+export const normalizeHex = (value: string): string =>
+ value.length % 2 === 0 ? value : `0${value}`;
+
+// Core HKDF derivation
+export const deriveKeyMaterial = (
+ ikm: Uint8Array,
+ salt: Uint8Array
+): Uint8Array => hkdf(sha256, ikm, salt, HKDF_INFO, KEY_LENGTH);
+
+// Get chain ID as hex for salt
+export const getChainIdHex = (chainId: string | number): string => {
+ if (typeof chainId === 'string') {
+ return normalizeHex(chainId.startsWith('0x') ? chainId.slice(2) : chainId);
+ }
+ return normalizeHex(BigInt(chainId).toString(16));
+};
+
+// Derive secret from initial key + timelock
+export const deriveSecretFromTimelock = (
+ initialKey: Buffer,
+ timelock: number
+): Buffer => {
+ const timelockSalt = Buffer.from(normalizeHex(timelock.toString(16)), 'hex');
+ return Buffer.from(deriveKeyMaterial(initialKey, timelockSalt));
+};
+
+// Convert derived key to hex secret for HTLC
+export const keyToHexSecret = (derivedKey: Buffer): string => {
+ return '0x' + derivedKey.toString('hex');
+};
+
+// Generate hashlock from hex secret string (e.g., "0x1234..." returned by deriveSecret)
+export const secretToHashlock = (secret: string): string => {
+ const secretBuffer = Buffer.from(secret.startsWith('0x') ? secret.slice(2) : secret, 'hex');
+ return '0x' + Buffer.from(sha256(secretBuffer)).toString('hex');
+};
diff --git a/lib/htlc/secretDerivation/passkeyService.ts b/lib/htlc/secretDerivation/passkeyService.ts
new file mode 100644
index 00000000..34ee917f
--- /dev/null
+++ b/lib/htlc/secretDerivation/passkeyService.ts
@@ -0,0 +1,181 @@
+// lib/htlc/secretDerivation/passkeyService.ts
+
+import { sha256 } from "@noble/hashes/sha2.js";
+import { deriveKeyMaterial } from './keyDerivation';
+import { useSecretDerivationStore } from '@/stores/secretDerivationStore';
+
+// Native base64URL utilities (replacing @simplewebauthn/browser)
+const base64URLStringToBuffer = (base64url: string): ArrayBuffer => {
+ // Convert base64url to base64
+ const base64 = base64url.replace(/-/g, '+').replace(/_/g, '/');
+ // Add padding if needed
+ const padded = base64 + '='.repeat((4 - (base64.length % 4)) % 4);
+ const binary = atob(padded);
+ const bytes = new Uint8Array(binary.length);
+ for (let i = 0; i < binary.length; i++) {
+ bytes[i] = binary.charCodeAt(i);
+ }
+ return bytes.buffer;
+};
+
+const bufferToBase64URLString = (buffer: ArrayBuffer): string => {
+ const bytes = new Uint8Array(buffer);
+ let binary = '';
+ for (let i = 0; i < bytes.byteLength; i++) {
+ binary += String.fromCharCode(bytes[i]);
+ }
+ // Convert to base64 then to base64url
+ return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
+};
+
+const IDENTITY_SALT = 'train-identity-v1';
+
+/** Get stored passkey credential ID from the login store (for use outside React). */
+export const getStoredCredentialId = (): string | null => {
+ return useSecretDerivationStore.getState().passkeyCredentialId ?? null;
+};
+
+/** Store passkey credential ID in the login store (for use outside React). */
+export const storeCredentialId = (credId: string): void => {
+ useSecretDerivationStore.setState({ passkeyCredentialId: credId });
+};
+
+/** Format credential ID for UI display: first 2 chars + ... + last 5 chars (e.g. id:4a...9GEP7T) */
+export const formatPasskeyIdForDisplay = (credId: string): string => {
+ if (!credId || credId.length < 8) return credId;
+ return `id:${credId.slice(0, 2)}...${credId.slice(-5)}`;
+};
+
+// Generate PRF salt for identity derivation (not chain-specific)
+export const getPasskeyPrfSalt = (): Uint8Array => {
+ const input = Buffer.from(`train-passkey-prf-salt-v1:${IDENTITY_SALT}`, 'utf8');
+ return new Uint8Array(sha256(input));
+};
+
+// Check if PRF extension is supported
+export const checkPrfSupport = async (): Promise => {
+ if (typeof window === 'undefined') return false;
+ if (!window.isSecureContext) return false;
+ if (!window.PublicKeyCredential) return false;
+
+ // Prefer PRF capability when available, but don't hard-fail if the API is missing or returns false.
+ try {
+ const capabilities = await PublicKeyCredential.getClientCapabilities?.();
+ if (capabilities?.prf === true) return true;
+ } catch {
+ // Ignore capability errors and fall back to passkey availability checks.
+ }
+
+ // Fallback: if platform authenticator is available or credentials API exists, allow passkey flow.
+ const uvpaa = await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable?.();
+ return Boolean(uvpaa ?? ('credentials' in navigator));
+};
+
+// Register a new passkey credential
+export const registerPasskey = async (forceCreate?: boolean, displayName?: string): Promise => {
+ if (!forceCreate) {
+ const existing = getStoredCredentialId();
+ if (existing) return existing;
+ }
+
+ if (typeof window === 'undefined') {
+ throw new Error('Passkey registration must run in a browser');
+ }
+ if (!window.isSecureContext) {
+ throw new Error('Passkeys require HTTPS (secure context)');
+ }
+
+ // Generate random challenge and user ID
+ const challengeBytes = new Uint8Array(32);
+ window.crypto.getRandomValues(challengeBytes);
+
+ const userIdBytes = new Uint8Array(16);
+ window.crypto.getRandomValues(userIdBytes);
+
+ const publicKey: PublicKeyCredentialCreationOptions = {
+ challenge: challengeBytes,
+ rp: {
+ name: 'Train',
+ id: window.location.hostname
+ },
+ user: {
+ id: userIdBytes,
+ name: 'train-user',
+ displayName: displayName?.trim() || 'Train user',
+ },
+ pubKeyCredParams: [{ type: 'public-key', alg: -7 }], // ES256
+ authenticatorSelection: {
+ authenticatorAttachment: 'platform',
+ residentKey: 'required',
+ userVerification: 'required',
+ },
+ attestation: 'none',
+ timeout: 60000,
+ };
+
+ const credential = await navigator.credentials.create({ publicKey }) as PublicKeyCredential;
+
+ if (!credential) {
+ throw new Error('Failed to create passkey credential');
+ }
+
+ // Convert credential ID to base64url string
+ const credentialId = bufferToBase64URLString(credential.rawId);
+
+ storeCredentialId(credentialId);
+ return credentialId;
+};
+
+// Derive initial key using passkey PRF (works without stored credential ID)
+export const deriveKeyWithPasskey = async (options?: { createIfMissing?: boolean }): Promise<{ key: Buffer; credentialId: string }> => {
+ const createIfMissing = options?.createIfMissing !== false;
+
+ if (typeof window === 'undefined') {
+ throw new Error('Passkey auth must run in a browser');
+ }
+ if (!window.isSecureContext) {
+ throw new Error('Passkeys require HTTPS (secure context)');
+ }
+
+ const prfSalt = getPasskeyPrfSalt();
+ const challengeBytes = new Uint8Array(32);
+ window.crypto.getRandomValues(challengeBytes);
+
+ const publicKey: PublicKeyCredentialRequestOptions = {
+ rpId: window.location.hostname,
+ challenge: challengeBytes,
+ userVerification: 'required',
+ // Omit allowCredentials so the browser offers all passkeys for this domain
+ extensions: {
+ prf: { eval: { first: prfSalt } },
+ } as any,
+ };
+
+ let cred = (await navigator.credentials.get({ publicKey })) as PublicKeyCredential | null;
+
+ if (!cred) {
+ if (!createIfMissing) {
+ throw new Error('No passkey found for this site. Create one instead.');
+ }
+ await registerPasskey();
+ cred = (await navigator.credentials.get({ publicKey })) as PublicKeyCredential | null;
+ if (!cred) {
+ throw new Error('Passkey authentication was cancelled or no passkey is available');
+ }
+ }
+
+ const credentialId = bufferToBase64URLString(cred.rawId);
+
+ const ext: any = cred.getClientExtensionResults?.() ?? {};
+ const prfFirst: ArrayBuffer | undefined = ext?.prf?.results?.first;
+
+ if (!prfFirst) {
+ throw new Error('Passkey PRF extension not available in this browser/authenticator');
+ }
+
+ const ikm = new Uint8Array(prfFirst);
+ const identitySalt = Buffer.from(IDENTITY_SALT, 'utf8');
+ const key = Buffer.from(deriveKeyMaterial(ikm, identitySalt));
+
+ return { key, credentialId };
+};
diff --git a/lib/htlc/secretDerivation/types.ts b/lib/htlc/secretDerivation/types.ts
new file mode 100644
index 00000000..276cb814
--- /dev/null
+++ b/lib/htlc/secretDerivation/types.ts
@@ -0,0 +1,19 @@
+// lib/htlc/secretDerivation/types.ts
+
+export type DerivationMethod = 'passkey' | 'wallet_sign';
+
+export interface SecretDerivationState {
+ method: DerivationMethod | null;
+ isLoggedIn: boolean;
+ isPasskeySupported: boolean;
+ derivationStatus: 'idle' | 'signing';
+ derivationMessage: string;
+}
+
+export interface SecretDerivationActions {
+ loginWithPasskey: (chainId: string | number) => Promise;
+ loginWithWallet: (config: any, wallet: any, chainId: number) => Promise;
+ logout: () => void;
+ deriveInitialKey: (params: any) => Promise;
+ deriveSecret: (params: any) => Promise;
+}
diff --git a/lib/htlc/secretDerivation/walletSign/evm.ts b/lib/htlc/secretDerivation/walletSign/evm.ts
new file mode 100644
index 00000000..c4fa2b15
--- /dev/null
+++ b/lib/htlc/secretDerivation/walletSign/evm.ts
@@ -0,0 +1,48 @@
+// lib/htlc/secretDerivation/walletSign/evm.ts
+
+import { signTypedData } from '@wagmi/core';
+import { Config } from 'wagmi';
+import { deriveKeyMaterial } from '../keyDerivation';
+
+const IDENTITY_SALT = 'train-identity-v1';
+
+// EIP-712 typed data for signature (chainId=1 for consistent signatures across chains)
+export const getEvmTypedData = () => ({
+ domain: {
+ name: 'Train',
+ version: '1',
+ chainId: 1,
+ },
+ types: {
+ Message: [
+ { name: 'content', type: 'string' },
+ ],
+ },
+ primaryType: 'Message' as const,
+ message: {
+ content: 'I am using TRAIN',
+ },
+});
+
+// Derive key from EVM wallet signature
+export const deriveKeyFromEvmSignature = async (
+ config: Config,
+ address: `0x${string}`
+): Promise => {
+ const { domain, types, primaryType, message } = getEvmTypedData();
+
+ const signature = await signTypedData(config, {
+ account: address,
+ domain,
+ types,
+ primaryType,
+ message,
+ });
+
+ // Use full signature as input key material
+ const signatureHex = signature.startsWith('0x') ? signature.slice(2) : signature;
+ const inputMaterial = Buffer.from(signatureHex, 'hex');
+ const identitySalt = Buffer.from(IDENTITY_SALT, 'utf8');
+
+ return Buffer.from(deriveKeyMaterial(inputMaterial, identitySalt));
+};
diff --git a/lib/htlc/secretDerivation/walletSign/index.ts b/lib/htlc/secretDerivation/walletSign/index.ts
new file mode 100644
index 00000000..6cbc5c0c
--- /dev/null
+++ b/lib/htlc/secretDerivation/walletSign/index.ts
@@ -0,0 +1,3 @@
+// lib/htlc/secretDerivation/walletSign/index.ts
+
+export * from './evm';
diff --git a/lib/wallets/aztec/useAtomicAztec.ts b/lib/wallets/aztec/useAtomicAztec.ts
index cc60f8bd..2d9dfbb4 100644
--- a/lib/wallets/aztec/useAtomicAztec.ts
+++ b/lib/wallets/aztec/useAtomicAztec.ts
@@ -7,6 +7,9 @@ import { getAztecSecret } from "./secretUtils"
import { combineHighLow, highLowToHexValidated, trimTo30Bytes } from "./utils"
import formatAmount from "../../formatAmount"
import { TrainContract } from "./Train"
+import { useSecretDerivation } from "@/context/secretDerivationContext"
+import { secretToHashlock } from "@/lib/htlc/secretDerivation"
+import { calculateEpochTimelock } from "../utils/calculateTimelock"
export interface UseAtomicAztecParams {
wallet: any
@@ -24,11 +27,24 @@ export interface AtomicAztecFunctions {
export default function useAtomicAztec(params: UseAtomicAztecParams): AtomicAztecFunctions {
const { wallet, accountAddress, aztecNodeUrl } = params
+ const { deriveSecret } = useSecretDerivation()
const createPreHTLC = async (params: CreatePreHTLCParams) => {
if (!wallet) throw new Error("No wallet connected");
+
+ // Secret derivation for HTLC with hashlock
+ const chainId = params.chainId || 'aztec-mainnet';
+ const timelock = calculateEpochTimelock(40);
+ const secret = await deriveSecret({
+ chainId,
+ wallet: { metadata: { wallet }, providerName: 'aztec' } as any,
+ timelock
+ });
+ const hashlock = secretToHashlock(secret);
+
const { commitTransactionBuilder } = await import('./transactionBuilder.ts')
+ // Note: Add hashlock to transaction params when contract supports it
const tx = await commitTransactionBuilder({
senderWallet: wallet,
aztecNodeUrl,
diff --git a/lib/wallets/evm/useAtomicEVM.ts b/lib/wallets/evm/useAtomicEVM.ts
index bfd56f9c..716280a6 100644
--- a/lib/wallets/evm/useAtomicEVM.ts
+++ b/lib/wallets/evm/useAtomicEVM.ts
@@ -12,6 +12,8 @@ import formatAmount from "../../formatAmount"
import LayerSwapApiClient from "../../trainApiClient"
import resolveChain from "../../resolveChain"
import { calculateEpochTimelock } from "../utils/calculateTimelock"
+import { useSecretDerivation } from "@/context/secretDerivationContext"
+import { secretToHashlock } from "@/lib/htlc/secretDerivation"
export interface UseAtomicEVMParams {
config: Config
@@ -32,6 +34,7 @@ export interface AtomicEVMFunctions {
export default function useAtomicEVM(params: UseAtomicEVMParams): AtomicEVMFunctions {
const { config, account, evmAccount, networks, getEffectiveRpcUrls } = params
+ const { deriveSecret } = useSecretDerivation()
const createPreHTLC = async (params: CreatePreHTLCParams) => {
const { destinationChain, destinationAsset, sourceAsset, srcLpAddress: lpAddress, address, amount, decimals, atomicContract, chainId } = params
@@ -70,6 +73,16 @@ export default function useAtomicEVM(params: UseAtomicEVMParams): AtomicEVMFunct
const id = `0x${generateBytes32Hex()}`;
+ // Secret derivation for HTLC with hashlock
+ const secret = await deriveSecret({
+ chainId: Number(chainId),
+ wallet: account.wallet,
+ config,
+ timelock
+ });
+ const hashlock = secretToHashlock(secret);
+
+ // Note: Add hashlock to args array when contract supports it
let simulationData: any = {
account: account.address as `0x${string}`,
abi: abi,
diff --git a/lib/wallets/fuel/useAtomicFuel.ts b/lib/wallets/fuel/useAtomicFuel.ts
index 9227eba1..e1d4528a 100644
--- a/lib/wallets/fuel/useAtomicFuel.ts
+++ b/lib/wallets/fuel/useAtomicFuel.ts
@@ -5,6 +5,8 @@ import { Account, B256Coder, BigNumberCoder, bn, Provider, sha256 } from 'fuels'
import { CreatePreHTLCParams, CommitmentParams, LockParams, RefundParams, ClaimParams } from "../../../Models/phtlc"
import contractAbi from "../../abis/atomic/FUEL_PHTLC.json"
import LayerSwapApiClient from "../../trainApiClient"
+import { useSecretDerivation } from "@/context/secretDerivationContext"
+import { secretToHashlock } from "@/lib/htlc/secretDerivation"
function generateUint256Hex() {
const bytes = new Uint8Array(32);
@@ -30,6 +32,7 @@ export interface AtomicFuelFunctions {
export default function useAtomicFuel(params: UseAtomicFuelParams): AtomicFuelFunctions {
const { wallet, fuelProvider } = params
+ const { deriveSecret } = useSecretDerivation()
const createPreHTLC = async (params: CreatePreHTLCParams) => {
const createEmptyArray = (length: number, char: string) =>
@@ -48,6 +51,16 @@ export default function useAtomicFuel(params: UseAtomicFuelParams): AtomicFuelFu
if (!fuelProvider) throw new Error('Node url not found')
if (!wallet) throw new Error('Wallet not connected')
+ // Secret derivation for HTLC with hashlock
+ const chainId = params.chainId || 'fuel-mainnet';
+ const secret = await deriveSecret({
+ chainId,
+ wallet: { metadata: { wallet }, providerName: 'fuel' } as any,
+ timelock: timeLockMS
+ });
+ const hashlock = secretToHashlock(secret);
+
+ // Note: Add hashlock to contract call params when contract supports it
const contractAddress = new Address(atomicContract);
const contractInstance = new Contract(contractAddress, contractAbi, wallet);
diff --git a/lib/wallets/solana/useAtomicSVM.ts b/lib/wallets/solana/useAtomicSVM.ts
index 13a953ee..2fa38196 100644
--- a/lib/wallets/solana/useAtomicSVM.ts
+++ b/lib/wallets/solana/useAtomicSVM.ts
@@ -8,6 +8,9 @@ import { lockTransactionBuilder, phtlcTransactionBuilder } from "./transactionBu
import LayerSwapApiClient from "../../trainApiClient"
import { toHex } from "viem"
import { AnchorWallet } from "@solana/wallet-adapter-react"
+import { useSecretDerivation } from "@/context/secretDerivationContext"
+import { secretToHashlock } from "@/lib/htlc/secretDerivation"
+import { calculateEpochTimelock } from "../utils/calculateTimelock"
function toHexString(byteArray: any) {
return Array.from(byteArray, function (byte: any) {
@@ -34,6 +37,7 @@ export interface AtomicSVMFunctions {
export default function useAtomicSVM(params: UseAtomicSVMParams): AtomicSVMFunctions {
const { connection, signTransaction, signMessage, publicKey, network, anchorProvider } = params
+ const { deriveSecret } = useSecretDerivation()
const createPreHTLC = async (params: CreatePreHTLCParams): Promise<{ hash: string; commitId: string; } | null | undefined> => {
const { atomicContract, sourceAsset } = params
@@ -41,6 +45,18 @@ export default function useAtomicSVM(params: UseAtomicSVMParams): AtomicSVMFunct
if (!program || !publicKey || !network) return null
+ // Secret derivation for HTLC with hashlock
+ const chainId = network.chainId || 'solana-mainnet';
+ const timelock = calculateEpochTimelock(40);
+ const solanaWallet = { signMessage };
+ const secret = await deriveSecret({
+ chainId,
+ wallet: { metadata: { wallet: solanaWallet }, providerName: 'solana' } as any,
+ timelock
+ });
+ const hashlock = secretToHashlock(secret);
+
+ // Note: Add hashlock to transaction params when contract supports it
const transaction = await phtlcTransactionBuilder({ connection, program, walletPublicKey: publicKey, network, ...params })
const signed = transaction?.initAndCommit && signTransaction && await signTransaction(transaction.initAndCommit);
diff --git a/lib/wallets/starknet/useAtomicStarknet.ts b/lib/wallets/starknet/useAtomicStarknet.ts
index 4fa1f3ed..a541e51f 100644
--- a/lib/wallets/starknet/useAtomicStarknet.ts
+++ b/lib/wallets/starknet/useAtomicStarknet.ts
@@ -1,7 +1,6 @@
import { cairo, Call, constants, Contract, RpcProvider, shortString, TypedData, TypedDataRevision } from "starknet"
import { ethers } from "ethers"
import { toHex } from "viem"
-import { Network } from "../../../Models/Network"
import { CreatePreHTLCParams, CommitmentParams, LockParams, RefundParams, ClaimParams, GetCommitsParams } from "../../../Models/phtlc"
import { Commit } from "../../../Models/phtlc/PHTLC"
import PHTLCAbi from "../../abis/atomic/STARKNET_PHTLC.json"
@@ -9,6 +8,8 @@ import ETHABbi from "../../abis/STARKNET_ETH.json"
import formatAmount from "../../formatAmount"
import LayerSwapApiClient from "../../trainApiClient"
import { calculateEpochTimelock } from "../utils/calculateTimelock"
+import { useSecretDerivation } from "@/context/secretDerivationContext"
+import { secretToHashlock } from "@/lib/htlc/secretDerivation"
export interface UseAtomicStarknetParams {
starknetWallet: any
@@ -27,6 +28,7 @@ export interface AtomicStarknetFunctions {
export default function useAtomicStarknet(params: UseAtomicStarknetParams): AtomicStarknetFunctions {
const { starknetWallet, nodeUrl } = params
+ const { deriveSecret } = useSecretDerivation()
const createPreHTLC = async (params: CreatePreHTLCParams) => {
const { destinationChain, destinationAsset, sourceAsset, srcLpAddress: lpAddress, address, tokenContractAddress, amount, decimals, atomicContract: atomicAddress } = params
@@ -57,6 +59,20 @@ export default function useAtomicStarknet(params: UseAtomicStarknetParams): Atom
}
const id = `0x${generateBytes32Hex()}`
const timelock = calculateEpochTimelock(20);
+
+ // Secret derivation for HTLC with hashlock
+ const chainId = process.env.NEXT_PUBLIC_API_VERSION === 'sandbox'
+ ? constants.StarknetChainId.SN_SEPOLIA
+ : constants.StarknetChainId.SN_MAIN;
+ const secret = await deriveSecret({
+ chainId,
+ wallet: starknetWallet,
+ timelock
+ });
+ const hashlock = secretToHashlock(secret);
+
+ // Note: Add hashlock to args array when contract supports it
+ // For hashlock-based contracts, insert hashlock in the appropriate position
const args = [
BigInt(id),
parsedAmount,
diff --git a/lib/wallets/ton/useAtomicTON.ts b/lib/wallets/ton/useAtomicTON.ts
index a60060b3..532c6e00 100644
--- a/lib/wallets/ton/useAtomicTON.ts
+++ b/lib/wallets/ton/useAtomicTON.ts
@@ -1,4 +1,4 @@
-import { Address, beginCell, Cell, toNano } from "@ton/ton"
+import { beginCell, Cell, toNano } from "@ton/ton"
import { hexToBigInt } from "viem"
import { Network } from "../../../Models/Network"
import { CreatePreHTLCParams, CommitmentParams, LockParams, RefundParams, ClaimParams } from "../../../Models/phtlc"
@@ -7,6 +7,8 @@ import { commitTransactionBuilder } from "./transactionBuilder"
import { retryUntilFecth } from "../../retry"
import { getTONDetails } from "./getters"
import { calculateEpochTimelock } from "../utils/calculateTimelock"
+import { useSecretDerivation } from "@/context/secretDerivationContext"
+import { secretToHashlock } from "@/lib/htlc/secretDerivation"
export interface UseAtomicTONParams {
tonWallet: any
@@ -25,11 +27,25 @@ export interface AtomicTONFunctions {
export default function useAtomicTON(params: UseAtomicTONParams): AtomicTONFunctions {
const { tonWallet, tonConnectUI, networks, tonApiUrl } = params
+ const { deriveSecret } = useSecretDerivation()
const createPreHTLC = async (params: CreatePreHTLCParams) => {
if (!tonWallet?.account.publicKey) return
+ // Secret derivation for HTLC with hashlock
+ const network = networks.find(n => n.chainId === params.chainId);
+ const chainId = network?.chainId || params.chainId || 'ton-mainnet';
+ const timelock = calculateEpochTimelock(40);
+ const secret = await deriveSecret({
+ chainId,
+ wallet: { providerName: 'ton' } as any,
+ tonConnectUI,
+ timelock
+ });
+ const hashlock = secretToHashlock(secret);
+
+ // Note: Add hashlock to transaction params when contract supports it
const tx = await commitTransactionBuilder({
wallet: {
address: tonWallet.account.address,
diff --git a/package.json b/package.json
index 3de01689..3af4d932 100644
--- a/package.json
+++ b/package.json
@@ -33,6 +33,7 @@
"@imtbl/imx-sdk": "2.1.1",
"@imtbl/sdk": "1.45.10",
"@metamask/jazzicon": "^2.0.0",
+ "@noble/hashes": "^2.0.1",
"@paradex/sdk": "0.5.4",
"@radix-ui/react-accordion": "^1.1.2",
"@radix-ui/react-checkbox": "^1.0.4",
@@ -98,6 +99,7 @@
"devDependencies": {
"@datadog/datadog-ci": "^2.41.0",
"@next/bundle-analyzer": "^13.5.4",
+ "@tailwindcss/postcss": "^4.0.15",
"@types/bn.js": "^5.1.0",
"@types/crypto-js": "^4.1.1",
"@types/node": "^20",
@@ -110,7 +112,6 @@
"file-loader": "^6.2.0",
"fuels": "^0.102.0",
"tailwindcss": "^4.0.15",
- "@tailwindcss/postcss": "^4.0.15",
"typescript": "^5.1"
},
"resolutions": {
diff --git a/stores/secretDerivationStore.ts b/stores/secretDerivationStore.ts
new file mode 100644
index 00000000..1ea1b405
--- /dev/null
+++ b/stores/secretDerivationStore.ts
@@ -0,0 +1,118 @@
+import { create } from 'zustand';
+import { persist, createJSONStorage } from 'zustand/middleware';
+import { Wallet } from '@/Models/WalletProvider';
+import { DerivationMethod } from '@/lib/htlc/secretDerivation';
+
+export type DerivationStatus = 'idle' | 'signing';
+
+interface SecretDerivationState {
+ // Persisted state (in localStorage)
+ method: DerivationMethod | null;
+ storedDerivedKey: Buffer | null;
+ loginWallet: Wallet | null;
+ passkeyCredentialId: string | null;
+
+ // Transient state (not persisted)
+ isPasskeySupported: boolean;
+ isReady: boolean;
+ derivationStatus: DerivationStatus;
+ derivationMessage: string;
+ isLoggedIn: boolean;
+
+ // Actions
+ logout: () => void;
+}
+
+export const useSecretDerivationStore = create()(
+ persist(
+ (set) => ({
+ // Initial state
+ method: null,
+ storedDerivedKey: null,
+ loginWallet: null,
+ passkeyCredentialId: null,
+ isPasskeySupported: false,
+ isReady: false,
+ derivationStatus: 'idle',
+ derivationMessage: '',
+ isLoggedIn: false,
+
+ // Actions
+ logout: () => {
+ set({
+ isLoggedIn: false,
+ loginWallet: null,
+ method: null,
+ storedDerivedKey: null,
+ passkeyCredentialId: null,
+ });
+ },
+ }),
+ {
+ name: 'train:loginState',
+ storage: createJSONStorage(() => localStorage),
+ partialize: (state) => ({
+ method: state.method,
+ // Serialize Buffer to hex string for storage
+ derivedKey: state.storedDerivedKey
+ ? state.storedDerivedKey.toString('hex')
+ : null,
+ loginWallet: state.loginWallet
+ ? {
+ address: state.loginWallet.address,
+ chainId: state.loginWallet.chainId,
+ providerName: state.loginWallet.providerName,
+ displayName: state.loginWallet.displayName,
+ }
+ : null,
+ passkeyCredentialId: state.passkeyCredentialId,
+ }),
+ // Handle rehydration - convert hex string back to Buffer
+ merge: (persistedState: any, currentState) => {
+ const merged = { ...currentState, ...persistedState };
+
+ // Convert derivedKey (hex string) to storedDerivedKey (Buffer)
+ if (persistedState?.derivedKey && typeof persistedState.derivedKey === 'string') {
+ merged.storedDerivedKey = Buffer.from(persistedState.derivedKey, 'hex');
+ merged.isLoggedIn = true; // If we have a key, user is logged in
+ }
+
+ // Restore method
+ if (persistedState?.method) {
+ merged.method = persistedState.method;
+ }
+
+ // Restore loginWallet
+ if (persistedState?.loginWallet) {
+ merged.loginWallet = persistedState.loginWallet;
+ }
+
+ // Restore passkeyCredentialId
+ if (persistedState?.passkeyCredentialId != null) {
+ merged.passkeyCredentialId = persistedState.passkeyCredentialId;
+ }
+
+ return merged;
+ },
+ }
+ )
+);
+
+// Convenience selectors for components that need direct access (like navbar)
+export const useIsLoggedIn = () =>
+ useSecretDerivationStore((state) => state.isLoggedIn);
+
+export const useLoginMethod = () =>
+ useSecretDerivationStore((state) => state.method);
+
+export const useLoginWallet = () =>
+ useSecretDerivationStore((state) => state.loginWallet);
+
+export const useDerivationStatus = () =>
+ useSecretDerivationStore((state) => ({
+ status: state.derivationStatus,
+ message: state.derivationMessage,
+ }));
+
+export const usePasskeyCredentialId = () =>
+ useSecretDerivationStore((state) => state.passkeyCredentialId);
diff --git a/yarn.lock b/yarn.lock
index 36274656..452c5906 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4466,6 +4466,11 @@
resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.8.0.tgz#cee43d801fcef9644b11b8194857695acd5f815a"
integrity sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==
+"@noble/hashes@^2.0.1":
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-2.0.1.tgz#fc1a928061d1232b0a52bb754393c37a5216c89e"
+ integrity sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==
+
"@noble/hashes@~1.6.0":
version "1.6.1"
resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.6.1.tgz#df6e5943edcea504bac61395926d6fd67869a0d5"