From 019a3d132891b522254b1db2fe698f9eb09257e4 Mon Sep 17 00:00:00 2001 From: Kefan Cao Date: Thu, 16 Oct 2025 01:30:18 -0400 Subject: [PATCH 01/21] test --- src/providers/TurnkeyAuthProvider.tsx | 33 +++++++++++++++++++-------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/src/providers/TurnkeyAuthProvider.tsx b/src/providers/TurnkeyAuthProvider.tsx index a8086507b..a7bdf1d25 100644 --- a/src/providers/TurnkeyAuthProvider.tsx +++ b/src/providers/TurnkeyAuthProvider.tsx @@ -1,4 +1,12 @@ -import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'; +import { + createContext, + useCallback, + useContext, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; import { logBonsaiError, logBonsaiInfo } from '@/bonsai/logs'; import { selectIndexerUrl } from '@/bonsai/socketSelectors'; @@ -77,6 +85,7 @@ const useTurnkeyAuthContext = () => { const [emailSignInStatus, setEmailSignInStatus] = useState< 'idle' | 'loading' | 'success' | 'error' >('idle'); + const processedEmailTokenRef = useRef(null); const { embeddedPublicKey, @@ -484,6 +493,13 @@ const useTurnkeyAuthContext = () => { ] ); + const handleEmailMagicLinkRef = useRef(handleEmailMagicLink); + + // Update the ref whenever handleEmailMagicLink changes + useEffect(() => { + handleEmailMagicLinkRef.current = handleEmailMagicLink; + }, [handleEmailMagicLink]); + const signInWithOtp = useCallback( async ({ userEmail }: { userEmail: string }) => { try { @@ -522,6 +538,7 @@ const useTurnkeyAuthContext = () => { setEmailToken(undefined); setEmailSignInStatus('idle'); setEmailSignInError(undefined); + processedEmailTokenRef.current = null; }, [searchParams, setSearchParams]); /* ----------------------------- Side Effects ----------------------------- */ @@ -559,19 +576,15 @@ const useTurnkeyAuthContext = () => { emailToken && targetPublicKeys?.publicKey && authIframeClient && - emailSignInStatus === 'idle' + emailSignInStatus === 'idle' && + processedEmailTokenRef.current !== emailToken ) { + processedEmailTokenRef.current = emailToken; track(AnalyticsEvents.TurnkeyLoginEmailToken({})); logBonsaiInfo('TurnkeyOnboarding', 'Attempting to handle email magic link'); - handleEmailMagicLink({ token: emailToken }); + handleEmailMagicLinkRef.current({ token: emailToken }); } - }, [ - emailToken, - targetPublicKeys?.publicKey, - authIframeClient, - handleEmailMagicLink, - emailSignInStatus, - ]); + }, [emailToken, targetPublicKeys?.publicKey, authIframeClient, emailSignInStatus]); const needsAddressUpload = useMemo(() => { return ( From a21127e6888436168bcbaf192767c9ffc31504b5 Mon Sep 17 00:00:00 2001 From: Kefan Cao Date: Wed, 5 Nov 2025 17:27:31 -0500 Subject: [PATCH 02/21] initial --- src/providers/TurnkeyAuthProvider.tsx | 90 ++++++++++++++++++- src/providers/TurnkeyWalletProvider.tsx | 3 +- src/types/turnkey.ts | 22 ++++- src/views/dialogs/OnboardingDialog/SignIn.tsx | 8 +- 4 files changed, 112 insertions(+), 11 deletions(-) diff --git a/src/providers/TurnkeyAuthProvider.tsx b/src/providers/TurnkeyAuthProvider.tsx index a7bdf1d25..c8d05db6c 100644 --- a/src/providers/TurnkeyAuthProvider.tsx +++ b/src/providers/TurnkeyAuthProvider.tsx @@ -11,7 +11,7 @@ import { import { logBonsaiError, logBonsaiInfo } from '@/bonsai/logs'; import { selectIndexerUrl } from '@/bonsai/socketSelectors'; import { useMutation } from '@tanstack/react-query'; -import { TurnkeyIndexedDbClient } from '@turnkey/sdk-browser'; +import { SessionType, TurnkeyIndexedDbClient } from '@turnkey/sdk-browser'; import { useTurnkey } from '@turnkey/sdk-react'; import { jwtDecode } from 'jwt-decode'; import { useSearchParams } from 'react-router-dom'; @@ -77,7 +77,7 @@ const useTurnkeyAuthContext = () => { const stringGetter = useStringGetter(); const indexerUrl = useAppSelector(selectIndexerUrl); const sourceAccount = useAppSelector(getSourceAccount); - const { indexedDbClient, authIframeClient } = useTurnkey(); + const { indexedDbClient, authIframeClient, passkeyClient } = useTurnkey(); const { dydxAddress: connectedDydxAddress, setWalletFromSignature, selectWallet } = useAccounts(); const [searchParams, setSearchParams] = useSearchParams(); const [emailToken, setEmailToken] = useState(); @@ -226,7 +226,9 @@ const useTurnkeyAuthContext = () => { handleEmailResponse({ userEmail, response }); setEmailSignInStatus('idle'); break; - case LoginMethod.Passkey: // TODO: handle passkey response + case LoginMethod.Passkey: + handlePasskeyResponse({ response }); + break; default: throw new Error('Current unsupported login method'); } @@ -341,6 +343,50 @@ const useTurnkeyAuthContext = () => { [onboardDydx, indexedDbClient, setWalletFromSignature, uploadAddress] ); + const handlePasskeyResponse = async ({ response }: { response: TurnkeyOAuthResponse }) => { + const { salt, dydxAddress: uploadedDydxAddress } = response as { + salt?: string; + dydxAddress?: string; + }; + + if (!passkeyClient) { + throw new Error('Passkey client is not available'); + } + await indexedDbClient!.resetKeyPair(); + const pubKey = await indexedDbClient!.getPublicKey(); + if (!pubKey) { + throw new Error('No public key available for passkey session'); + } + // Authenticate with the user's passkey for the returned sub-organization + await passkeyClient.loginWithPasskey({ + sessionType: SessionType.READ_WRITE, + publicKey: pubKey, + expirationSeconds: (60 * 15).toString(), // 15 minutes + organizationId: '18af402a-684a-488d-a054-3c5c688eb7d5', + }); + const derivedDydxAddress = await onboardDydx({ + salt, + setWalletFromSignature, + tkClient: indexedDbClient, + }); + + if (uploadedDydxAddress === '' && derivedDydxAddress) { + try { + await uploadAddress({ tkClient: indexedDbClient, dydxAddress: derivedDydxAddress }); + } catch (uploadAddressError) { + if ( + uploadAddressError instanceof Error && + !uploadAddressError.message.includes('Dydx address already uploaded') + ) { + throw uploadAddressError; + } + } + } + + setEmailSignInStatus('success'); + setEmailSignInError(undefined); + }; + /* ----------------------------- Email Sign In ----------------------------- */ const handleEmailResponse = useCallback( @@ -541,6 +587,43 @@ const useTurnkeyAuthContext = () => { processedEmailTokenRef.current = null; }, [searchParams, setSearchParams]); + /* ----------------------------- Passkey Sign In ----------------------------- */ + + const signInWithPasskey = useCallback(async () => { + try { + if (!passkeyClient) { + throw new Error('Passkey client is not available'); + } + + const { encodedChallenge: challenge, attestation } = await passkeyClient.createUserPasskey({ + publicKey: { + user: { + name: 'test.dydx.com', + displayName: 'wallet.dydx.com', + }, + }, + }); + + const bodyWithAttestation: SignInBody = { + signinMethod: 'passkey', + challenge, + attestation: { + transports: attestation.transports, + attestationObject: attestation.attestationObject, + clientDataJson: attestation.clientDataJson, + credentialId: attestation.credentialId, + }, + }; + + sendSignInRequest({ + body: JSON.stringify(bodyWithAttestation), + loginMethod: LoginMethod.Passkey, + }); + } catch (error) { + logBonsaiError('TurnkeyOnboarding', 'Error signing in with passkey', { error }); + } + }, [passkeyClient, sendSignInRequest]); + /* ----------------------------- Side Effects ----------------------------- */ /** @@ -605,6 +688,7 @@ const useTurnkeyAuthContext = () => { isUploadingAddress, signInWithOauth, signInWithOtp, + signInWithPasskey, resetEmailSignInStatus, }; }; diff --git a/src/providers/TurnkeyWalletProvider.tsx b/src/providers/TurnkeyWalletProvider.tsx index 45e2668f4..37aef6601 100644 --- a/src/providers/TurnkeyWalletProvider.tsx +++ b/src/providers/TurnkeyWalletProvider.tsx @@ -201,8 +201,7 @@ const useTurnkeyWalletContext = () => { setWalletFromSignature: (signature: string) => Promise; tkClient?: TurnkeyIndexedDbClient; }) => { - const selectedTurnkeyWallet = primaryTurnkeyWallet ?? (await getPrimaryUserWallets(tkClient)); - + const selectedTurnkeyWallet = await getPrimaryUserWallets(tkClient); const ethAccount = selectedTurnkeyWallet?.accounts.find( (account) => account.addressFormat === AddressFormat.Ethereum ); diff --git a/src/types/turnkey.ts b/src/types/turnkey.ts index 2539e6d37..43a42590b 100644 --- a/src/types/turnkey.ts +++ b/src/types/turnkey.ts @@ -49,7 +49,7 @@ export type GoogleIdTokenPayload = { export type SignInBody = | { - signinMethod: 'social' | 'passkey'; + signinMethod: 'social'; targetPublicKey: string; provider: 'google' | 'apple'; oidcToken: string; @@ -57,9 +57,27 @@ export type SignInBody = } | { signinMethod: 'email'; - targetPublicKey: string; + targetPublicKey?: string; userEmail: string; magicLink: string; + } + | { + signinMethod: 'passkey'; + // In a full implementation these are required; left optional to allow + // initiating the flow and handling multi-step server responses. + challenge?: string; + attestation?: { + transports: Array< + | 'AUTHENTICATOR_TRANSPORT_BLE' + | 'AUTHENTICATOR_TRANSPORT_INTERNAL' + | 'AUTHENTICATOR_TRANSPORT_NFC' + | 'AUTHENTICATOR_TRANSPORT_USB' + | 'AUTHENTICATOR_TRANSPORT_HYBRID' + >; + attestationObject: string; // base64url + clientDataJson: string; // base64url + credentialId: string; // base64url + }; }; export type TurnkeyEmailOnboardingData = { diff --git a/src/views/dialogs/OnboardingDialog/SignIn.tsx b/src/views/dialogs/OnboardingDialog/SignIn.tsx index f8551e5bd..599083dfd 100644 --- a/src/views/dialogs/OnboardingDialog/SignIn.tsx +++ b/src/views/dialogs/OnboardingDialog/SignIn.tsx @@ -52,7 +52,7 @@ export const SignIn = ({ const [email, setEmail] = useState(''); const [isLoading, setIsLoading] = useState(false); const { authIframeClient } = useTurnkey(); - const { signInWithOtp } = useTurnkeyAuth(); + const { signInWithOtp, signInWithPasskey } = useTurnkeyAuth(); const appTheme = useAppSelector(getAppTheme); const { tos, privacy } = useURLConfigs(); const displayedWallets = useDisplayedWallets(); @@ -138,11 +138,11 @@ export const SignIn = ({ <$HorizontalSeparatorFiller $isLightMode={appTheme === AppTheme.Light} /> - {/* <$OtherOptionButton + <$OtherOptionButton type={ButtonType.Button} action={ButtonAction.Base} size={ButtonSize.BasePlus} - onClick={onSignInWithPasskey} + onClick={signInWithPasskey} >
@@ -150,7 +150,7 @@ export const SignIn = ({
- */} + {displayedWallets .filter( From 6610936d430785b425e35a73f4d61a43679b50e3 Mon Sep 17 00:00:00 2001 From: Kefan Cao Date: Wed, 5 Nov 2025 22:51:05 -0500 Subject: [PATCH 03/21] sign in and credential imitation --- src/providers/TurnkeyAuthProvider.tsx | 51 +++++++++++++-------------- 1 file changed, 25 insertions(+), 26 deletions(-) diff --git a/src/providers/TurnkeyAuthProvider.tsx b/src/providers/TurnkeyAuthProvider.tsx index c8d05db6c..dd3dde21c 100644 --- a/src/providers/TurnkeyAuthProvider.tsx +++ b/src/providers/TurnkeyAuthProvider.tsx @@ -94,6 +94,7 @@ const useTurnkeyAuthContext = () => { onboardDydx, targetPublicKeys, getUploadAddressPayload, + fetchCredentialId, } = useTurnkeyWallet(); /* ----------------------------- Upload Address ----------------------------- */ @@ -352,18 +353,6 @@ const useTurnkeyAuthContext = () => { if (!passkeyClient) { throw new Error('Passkey client is not available'); } - await indexedDbClient!.resetKeyPair(); - const pubKey = await indexedDbClient!.getPublicKey(); - if (!pubKey) { - throw new Error('No public key available for passkey session'); - } - // Authenticate with the user's passkey for the returned sub-organization - await passkeyClient.loginWithPasskey({ - sessionType: SessionType.READ_WRITE, - publicKey: pubKey, - expirationSeconds: (60 * 15).toString(), // 15 minutes - organizationId: '18af402a-684a-488d-a054-3c5c688eb7d5', - }); const derivedDydxAddress = await onboardDydx({ salt, setWalletFromSignature, @@ -589,29 +578,39 @@ const useTurnkeyAuthContext = () => { /* ----------------------------- Passkey Sign In ----------------------------- */ - const signInWithPasskey = useCallback(async () => { + const signInWithPasskey = async () => { try { if (!passkeyClient) { throw new Error('Passkey client is not available'); } - const { encodedChallenge: challenge, attestation } = await passkeyClient.createUserPasskey({ - publicKey: { - user: { - name: 'test.dydx.com', - displayName: 'wallet.dydx.com', - }, - }, + await indexedDbClient!.resetKeyPair(); + const pubKey = await indexedDbClient!.getPublicKey(); + if (!pubKey) { + throw new Error('No public key available for passkey session'); + } + // Authenticate with the user's passkey for the returned sub-organization + await passkeyClient.loginWithPasskey({ + sessionType: SessionType.READ_WRITE, + publicKey: pubKey, + expirationSeconds: (60 * 15).toString(), // 15 minutes + organizationId: '18af402a-684a-488d-a054-3c5c688eb7d5', }); + const credentialId = await fetchCredentialId(indexedDbClient); + if (!credentialId) { + throw new Error('No user found'); + } + + // dummy body used to get salt. const bodyWithAttestation: SignInBody = { signinMethod: 'passkey', - challenge, + challenge: 'dummy', attestation: { - transports: attestation.transports, - attestationObject: attestation.attestationObject, - clientDataJson: attestation.clientDataJson, - credentialId: attestation.credentialId, + transports: ['AUTHENTICATOR_TRANSPORT_INTERNAL'], + attestationObject: 'dummy', + clientDataJson: 'dummy', + credentialId, }, }; @@ -622,7 +621,7 @@ const useTurnkeyAuthContext = () => { } catch (error) { logBonsaiError('TurnkeyOnboarding', 'Error signing in with passkey', { error }); } - }, [passkeyClient, sendSignInRequest]); + }; /* ----------------------------- Side Effects ----------------------------- */ From 2078998584d9ea4c9fd1809ab40a49282dd2ee6f Mon Sep 17 00:00:00 2001 From: Kefan Cao Date: Wed, 5 Nov 2025 22:51:12 -0500 Subject: [PATCH 04/21] fetch credentials --- src/providers/TurnkeyWalletProvider.tsx | 28 +++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/providers/TurnkeyWalletProvider.tsx b/src/providers/TurnkeyWalletProvider.tsx index 37aef6601..91a1b679f 100644 --- a/src/providers/TurnkeyWalletProvider.tsx +++ b/src/providers/TurnkeyWalletProvider.tsx @@ -123,6 +123,33 @@ const useTurnkeyWalletContext = () => { }, [authIframeClient]); /* ----------------------------- Onboarding Functions ----------------------------- */ + + // used to fetch the credential id for the passkey sign in + const fetchCredentialId = useCallback( + async (tkClient?: TurnkeyIndexedDbClient): Promise => { + if (turnkey == null || tkClient == null) { + return undefined; + } + // Try and get the current user + const token = await turnkey.getSession(); + + // If the user is not found, we assume the user is not logged in + if (!token?.expiry || token.expiry > Date.now()) { + return undefined; + } + + const { user: indexedDbUser } = await tkClient.getUser({ + organizationId: token.organizationId, + userId: token.userId, + }); + if (indexedDbUser.authenticators.length === 0) { + return undefined; + } + return indexedDbUser.authenticators[0]?.credentialId; + }, + [turnkey] + ); + const fetchUser = useCallback( async (tkClient?: TurnkeyIndexedDbClient): Promise => { const isIndexedDbFlow = tkClient instanceof TurnkeyIndexedDbClient; @@ -340,6 +367,7 @@ const useTurnkeyWalletContext = () => { isNewTurnkeyUser, endTurnkeySession, + fetchCredentialId, onboardDydx, getUploadAddressPayload, setIsNewTurnkeyUser, From 4f89d98e63182a32a6acf926f25ae7fdb5e633e6 Mon Sep 17 00:00:00 2001 From: Kefan Cao Date: Wed, 5 Nov 2025 22:51:30 -0500 Subject: [PATCH 05/21] add back primary turnkey wallet --- src/providers/TurnkeyWalletProvider.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/providers/TurnkeyWalletProvider.tsx b/src/providers/TurnkeyWalletProvider.tsx index 91a1b679f..240ba0511 100644 --- a/src/providers/TurnkeyWalletProvider.tsx +++ b/src/providers/TurnkeyWalletProvider.tsx @@ -228,7 +228,7 @@ const useTurnkeyWalletContext = () => { setWalletFromSignature: (signature: string) => Promise; tkClient?: TurnkeyIndexedDbClient; }) => { - const selectedTurnkeyWallet = await getPrimaryUserWallets(tkClient); + const selectedTurnkeyWallet = primaryTurnkeyWallet ?? (await getPrimaryUserWallets(tkClient)); const ethAccount = selectedTurnkeyWallet?.accounts.find( (account) => account.addressFormat === AddressFormat.Ethereum ); From 7655f3bd138e108d53d1226ca889b92393afa10b Mon Sep 17 00:00:00 2001 From: Kefan Cao Date: Wed, 5 Nov 2025 23:40:45 -0500 Subject: [PATCH 06/21] change --- src/providers/TurnkeyAuthProvider.tsx | 33 ++++++++------------------- 1 file changed, 10 insertions(+), 23 deletions(-) diff --git a/src/providers/TurnkeyAuthProvider.tsx b/src/providers/TurnkeyAuthProvider.tsx index dd3dde21c..c65dcf557 100644 --- a/src/providers/TurnkeyAuthProvider.tsx +++ b/src/providers/TurnkeyAuthProvider.tsx @@ -1,12 +1,4 @@ -import { - createContext, - useCallback, - useContext, - useEffect, - useMemo, - useRef, - useState, -} from 'react'; +import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'; import { logBonsaiError, logBonsaiInfo } from '@/bonsai/logs'; import { selectIndexerUrl } from '@/bonsai/socketSelectors'; @@ -85,7 +77,6 @@ const useTurnkeyAuthContext = () => { const [emailSignInStatus, setEmailSignInStatus] = useState< 'idle' | 'loading' | 'success' | 'error' >('idle'); - const processedEmailTokenRef = useRef(null); const { embeddedPublicKey, @@ -528,13 +519,6 @@ const useTurnkeyAuthContext = () => { ] ); - const handleEmailMagicLinkRef = useRef(handleEmailMagicLink); - - // Update the ref whenever handleEmailMagicLink changes - useEffect(() => { - handleEmailMagicLinkRef.current = handleEmailMagicLink; - }, [handleEmailMagicLink]); - const signInWithOtp = useCallback( async ({ userEmail }: { userEmail: string }) => { try { @@ -573,7 +557,6 @@ const useTurnkeyAuthContext = () => { setEmailToken(undefined); setEmailSignInStatus('idle'); setEmailSignInError(undefined); - processedEmailTokenRef.current = null; }, [searchParams, setSearchParams]); /* ----------------------------- Passkey Sign In ----------------------------- */ @@ -658,15 +641,19 @@ const useTurnkeyAuthContext = () => { emailToken && targetPublicKeys?.publicKey && authIframeClient && - emailSignInStatus === 'idle' && - processedEmailTokenRef.current !== emailToken + emailSignInStatus === 'idle' ) { - processedEmailTokenRef.current = emailToken; track(AnalyticsEvents.TurnkeyLoginEmailToken({})); logBonsaiInfo('TurnkeyOnboarding', 'Attempting to handle email magic link'); - handleEmailMagicLinkRef.current({ token: emailToken }); + handleEmailMagicLink({ token: emailToken }); } - }, [emailToken, targetPublicKeys?.publicKey, authIframeClient, emailSignInStatus]); + }, [ + emailToken, + targetPublicKeys?.publicKey, + authIframeClient, + handleEmailMagicLink, + emailSignInStatus, + ]); const needsAddressUpload = useMemo(() => { return ( From e0b34e2c8b1ff7c1027823bbaff19bc96e725300 Mon Sep 17 00:00:00 2001 From: Kefan Cao Date: Wed, 5 Nov 2025 23:43:06 -0500 Subject: [PATCH 07/21] remove org id --- src/providers/TurnkeyAuthProvider.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/providers/TurnkeyAuthProvider.tsx b/src/providers/TurnkeyAuthProvider.tsx index c65dcf557..f3ac074f4 100644 --- a/src/providers/TurnkeyAuthProvider.tsx +++ b/src/providers/TurnkeyAuthProvider.tsx @@ -577,7 +577,6 @@ const useTurnkeyAuthContext = () => { sessionType: SessionType.READ_WRITE, publicKey: pubKey, expirationSeconds: (60 * 15).toString(), // 15 minutes - organizationId: '18af402a-684a-488d-a054-3c5c688eb7d5', }); const credentialId = await fetchCredentialId(indexedDbClient); From d33a92e16788ab2f6324a15c83d9a18048ed1b1a Mon Sep 17 00:00:00 2001 From: Kefan Cao Date: Wed, 5 Nov 2025 23:49:26 -0500 Subject: [PATCH 08/21] made optional --- src/providers/TurnkeyAuthProvider.tsx | 5 +---- src/types/turnkey.ts | 6 +++--- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/providers/TurnkeyAuthProvider.tsx b/src/providers/TurnkeyAuthProvider.tsx index f3ac074f4..4f143b850 100644 --- a/src/providers/TurnkeyAuthProvider.tsx +++ b/src/providers/TurnkeyAuthProvider.tsx @@ -587,11 +587,8 @@ const useTurnkeyAuthContext = () => { // dummy body used to get salt. const bodyWithAttestation: SignInBody = { signinMethod: 'passkey', - challenge: 'dummy', + challenge: credentialId, attestation: { - transports: ['AUTHENTICATOR_TRANSPORT_INTERNAL'], - attestationObject: 'dummy', - clientDataJson: 'dummy', credentialId, }, }; diff --git a/src/types/turnkey.ts b/src/types/turnkey.ts index 43a42590b..0b50f120a 100644 --- a/src/types/turnkey.ts +++ b/src/types/turnkey.ts @@ -67,15 +67,15 @@ export type SignInBody = // initiating the flow and handling multi-step server responses. challenge?: string; attestation?: { - transports: Array< + transports?: Array< | 'AUTHENTICATOR_TRANSPORT_BLE' | 'AUTHENTICATOR_TRANSPORT_INTERNAL' | 'AUTHENTICATOR_TRANSPORT_NFC' | 'AUTHENTICATOR_TRANSPORT_USB' | 'AUTHENTICATOR_TRANSPORT_HYBRID' >; - attestationObject: string; // base64url - clientDataJson: string; // base64url + attestationObject?: string; // base64url + clientDataJson?: string; // base64url credentialId: string; // base64url }; }; From 09918908794bdd4156e80eafc4dc1d900bcbad1c Mon Sep 17 00:00:00 2001 From: Kefan Cao Date: Wed, 5 Nov 2025 23:53:33 -0500 Subject: [PATCH 09/21] bring back callbacks --- src/providers/TurnkeyAuthProvider.tsx | 67 ++++++++++++++++----------- 1 file changed, 39 insertions(+), 28 deletions(-) diff --git a/src/providers/TurnkeyAuthProvider.tsx b/src/providers/TurnkeyAuthProvider.tsx index 4f143b850..8174da4d1 100644 --- a/src/providers/TurnkeyAuthProvider.tsx +++ b/src/providers/TurnkeyAuthProvider.tsx @@ -335,37 +335,48 @@ const useTurnkeyAuthContext = () => { [onboardDydx, indexedDbClient, setWalletFromSignature, uploadAddress] ); - const handlePasskeyResponse = async ({ response }: { response: TurnkeyOAuthResponse }) => { - const { salt, dydxAddress: uploadedDydxAddress } = response as { - salt?: string; - dydxAddress?: string; - }; + const handlePasskeyResponse = useCallback( + async ({ response }: { response: TurnkeyOAuthResponse }) => { + const { salt, dydxAddress: uploadedDydxAddress } = response as { + salt?: string; + dydxAddress?: string; + }; - if (!passkeyClient) { - throw new Error('Passkey client is not available'); - } - const derivedDydxAddress = await onboardDydx({ - salt, - setWalletFromSignature, - tkClient: indexedDbClient, - }); + if (!passkeyClient) { + throw new Error('Passkey client is not available'); + } + const derivedDydxAddress = await onboardDydx({ + salt, + setWalletFromSignature, + tkClient: indexedDbClient, + }); - if (uploadedDydxAddress === '' && derivedDydxAddress) { - try { - await uploadAddress({ tkClient: indexedDbClient, dydxAddress: derivedDydxAddress }); - } catch (uploadAddressError) { - if ( - uploadAddressError instanceof Error && - !uploadAddressError.message.includes('Dydx address already uploaded') - ) { - throw uploadAddressError; + if (uploadedDydxAddress === '' && derivedDydxAddress) { + try { + await uploadAddress({ tkClient: indexedDbClient, dydxAddress: derivedDydxAddress }); + } catch (uploadAddressError) { + if ( + uploadAddressError instanceof Error && + !uploadAddressError.message.includes('Dydx address already uploaded') + ) { + throw uploadAddressError; + } } } - } - setEmailSignInStatus('success'); - setEmailSignInError(undefined); - }; + setEmailSignInStatus('success'); + setEmailSignInError(undefined); + }, + [ + onboardDydx, + indexedDbClient, + setWalletFromSignature, + uploadAddress, + setEmailSignInStatus, + setEmailSignInError, + passkeyClient, + ] + ); /* ----------------------------- Email Sign In ----------------------------- */ @@ -561,7 +572,7 @@ const useTurnkeyAuthContext = () => { /* ----------------------------- Passkey Sign In ----------------------------- */ - const signInWithPasskey = async () => { + const signInWithPasskey = useCallback(async () => { try { if (!passkeyClient) { throw new Error('Passkey client is not available'); @@ -600,7 +611,7 @@ const useTurnkeyAuthContext = () => { } catch (error) { logBonsaiError('TurnkeyOnboarding', 'Error signing in with passkey', { error }); } - }; + }, [passkeyClient, indexedDbClient, fetchCredentialId, sendSignInRequest]); /* ----------------------------- Side Effects ----------------------------- */ From 5a7308dfed9e285b42357b624cb5216b76bf3dcd Mon Sep 17 00:00:00 2001 From: Kefan Cao Date: Wed, 5 Nov 2025 23:57:31 -0500 Subject: [PATCH 10/21] test flags --- src/lib/testFlags.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/lib/testFlags.ts b/src/lib/testFlags.ts index 5c67498e1..97ffacb76 100644 --- a/src/lib/testFlags.ts +++ b/src/lib/testFlags.ts @@ -72,6 +72,10 @@ class TestFlags { return this.booleanFlag(this.queryParams.apple_auth); } + get enablePasskeyAuth() { + return this.booleanFlag(this.queryParams.passkey_auth); + } + get spot() { return this.booleanFlag(this.queryParams.spot); } From 7881e3e2c3fe1e01760f5a73785f0afe13e4e566 Mon Sep 17 00:00:00 2001 From: Kefan Cao Date: Wed, 5 Nov 2025 23:57:51 -0500 Subject: [PATCH 11/21] test flag --- src/views/dialogs/OnboardingDialog/SignIn.tsx | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/src/views/dialogs/OnboardingDialog/SignIn.tsx b/src/views/dialogs/OnboardingDialog/SignIn.tsx index 599083dfd..d2c9f36b6 100644 --- a/src/views/dialogs/OnboardingDialog/SignIn.tsx +++ b/src/views/dialogs/OnboardingDialog/SignIn.tsx @@ -138,19 +138,21 @@ export const SignIn = ({ <$HorizontalSeparatorFiller $isLightMode={appTheme === AppTheme.Light} /> - <$OtherOptionButton - type={ButtonType.Button} - action={ButtonAction.Base} - size={ButtonSize.BasePlus} - onClick={signInWithPasskey} - > -
- - {stringGetter({ key: STRING_KEYS.SIGN_IN_PASSKEY })} -
+ {testFlags.enablePasskeyAuth && ( + <$OtherOptionButton + type={ButtonType.Button} + action={ButtonAction.Base} + size={ButtonSize.BasePlus} + onClick={signInWithPasskey} + > +
+ + {stringGetter({ key: STRING_KEYS.SIGN_IN_PASSKEY })} +
- - + + + )} {displayedWallets .filter( From b365cbeb729bac10f717b10746f7338c52fa594d Mon Sep 17 00:00:00 2001 From: Kefan Cao Date: Thu, 6 Nov 2025 14:38:54 -0500 Subject: [PATCH 12/21] passkey create --- src/providers/TurnkeyAuthProvider.tsx | 36 ++++++++++++++++++- src/views/dialogs/EmailSignInStatusDialog.tsx | 8 ++++- src/views/dialogs/SetupPasskeyDialog.tsx | 9 +++++ 3 files changed, 51 insertions(+), 2 deletions(-) diff --git a/src/providers/TurnkeyAuthProvider.tsx b/src/providers/TurnkeyAuthProvider.tsx index 8174da4d1..57638a110 100644 --- a/src/providers/TurnkeyAuthProvider.tsx +++ b/src/providers/TurnkeyAuthProvider.tsx @@ -69,7 +69,7 @@ const useTurnkeyAuthContext = () => { const stringGetter = useStringGetter(); const indexerUrl = useAppSelector(selectIndexerUrl); const sourceAccount = useAppSelector(getSourceAccount); - const { indexedDbClient, authIframeClient, passkeyClient } = useTurnkey(); + const { indexedDbClient, authIframeClient, passkeyClient, turnkey } = useTurnkey(); const { dydxAddress: connectedDydxAddress, setWalletFromSignature, selectWallet } = useAccounts(); const [searchParams, setSearchParams] = useSearchParams(); const [emailToken, setEmailToken] = useState(); @@ -572,6 +572,39 @@ const useTurnkeyAuthContext = () => { /* ----------------------------- Passkey Sign In ----------------------------- */ + const registerPasskey = useCallback(async () => { + try { + if (!passkeyClient || !indexedDbClient || !turnkey) { + throw new Error('Passkey client is not available'); + } + + const passkey = await passkeyClient.createUserPasskey({ + rp: { + id: 'dydx.trade', + name: '2FA Passkey', + }, + }); + + const session = await turnkey.getSession(); + if (!session || !passkey?.encodedChallenge || !passkey?.attestation) { + throw new Error('No session found'); + } + + await indexedDbClient.addUserAuth({ + userId: session.userId, + authenticators: [ + { + authenticatorName: 'Passkey', + challenge: passkey.encodedChallenge, + attestation: passkey.attestation, + }, + ], + }); + } catch (error) { + logBonsaiError('TurnkeyOnboarding', 'Error registering passkey', { error }); + } + }, [passkeyClient, indexedDbClient, turnkey]); + const signInWithPasskey = useCallback(async () => { try { if (!passkeyClient) { @@ -682,6 +715,7 @@ const useTurnkeyAuthContext = () => { signInWithOauth, signInWithOtp, signInWithPasskey, + registerPasskey, resetEmailSignInStatus, }; }; diff --git a/src/views/dialogs/EmailSignInStatusDialog.tsx b/src/views/dialogs/EmailSignInStatusDialog.tsx index 67a349126..aee5d70b7 100644 --- a/src/views/dialogs/EmailSignInStatusDialog.tsx +++ b/src/views/dialogs/EmailSignInStatusDialog.tsx @@ -204,7 +204,13 @@ export const EmailSignInStatusDialog = ({ if (showWelcomeContent) { await sleep(0); - dispatch(openDialog(DialogTypes.Deposit2({}))); + dispatch( + openDialog( + DialogTypes.SetupPasskey({ + onClose: () => dispatch(openDialog(DialogTypes.Deposit2({}))), + }) + ) + ); } }} > diff --git a/src/views/dialogs/SetupPasskeyDialog.tsx b/src/views/dialogs/SetupPasskeyDialog.tsx index 90efe8d32..852ae43b5 100644 --- a/src/views/dialogs/SetupPasskeyDialog.tsx +++ b/src/views/dialogs/SetupPasskeyDialog.tsx @@ -2,6 +2,8 @@ import { ReactNode, useCallback, useMemo } from 'react'; import { ButtonAction, ButtonSize, ButtonType } from '@/constants/buttons'; +import { useTurnkeyAuth } from '@/providers/TurnkeyAuthProvider'; + import { Button } from '@/components/Button'; import { Dialog } from '@/components/Dialog'; import { Icon, IconName } from '@/components/Icon'; @@ -13,6 +15,8 @@ export const SetupPasskeyDialog = ({ onClose: () => void; setIsOpen: (isOpen: boolean) => void; }) => { + const { registerPasskey } = useTurnkeyAuth(); + const modifiedSetIsOpen = useCallback( (isOpen: boolean) => { setIsOpen(isOpen); @@ -83,6 +87,10 @@ export const SetupPasskeyDialog = ({ css={{ color: 'var(--dialog-backgroundColor, var(--text-color-text-0))', }} + onClick={() => { + registerPasskey(); + modifiedSetIsOpen(false); + }} > Setup Passkey @@ -93,6 +101,7 @@ export const SetupPasskeyDialog = ({ type={ButtonType.Button} action={ButtonAction.Navigation} size={ButtonSize.BasePlus} + onClick={() => modifiedSetIsOpen(false)} > Skip From c51d967e1eec567232105506b8a6697acb4aef10 Mon Sep 17 00:00:00 2001 From: Kefan Cao Date: Thu, 6 Nov 2025 14:44:48 -0500 Subject: [PATCH 13/21] ff --- src/views/dialogs/EmailSignInStatusDialog.tsx | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/views/dialogs/EmailSignInStatusDialog.tsx b/src/views/dialogs/EmailSignInStatusDialog.tsx index aee5d70b7..64adcc985 100644 --- a/src/views/dialogs/EmailSignInStatusDialog.tsx +++ b/src/views/dialogs/EmailSignInStatusDialog.tsx @@ -22,6 +22,7 @@ import { openDialog } from '@/state/dialogs'; import { getSourceAccount, getTurnkeyEmailOnboardingData } from '@/state/walletSelectors'; import { calc } from '@/lib/do'; +import { testFlags } from '@/lib/testFlags'; import { sleep } from '@/lib/timeUtils'; export const EmailSignInStatusDialog = ({ @@ -204,13 +205,15 @@ export const EmailSignInStatusDialog = ({ if (showWelcomeContent) { await sleep(0); - dispatch( - openDialog( - DialogTypes.SetupPasskey({ - onClose: () => dispatch(openDialog(DialogTypes.Deposit2({}))), - }) - ) - ); + if (testFlags.enablePasskeyAuth) { + dispatch( + openDialog( + DialogTypes.SetupPasskey({ + onClose: () => dispatch(openDialog(DialogTypes.Deposit2({}))), + }) + ) + ); + } } }} > From 3bfd58d922c999053f139289e83e99c6e6663e87 Mon Sep 17 00:00:00 2001 From: Kefan Cao Date: Thu, 6 Nov 2025 14:38:54 -0500 Subject: [PATCH 14/21] passkey create --- src/providers/TurnkeyAuthProvider.tsx | 36 ++++++++++++++++++- src/views/dialogs/EmailSignInStatusDialog.tsx | 8 ++++- src/views/dialogs/SetupPasskeyDialog.tsx | 9 +++++ 3 files changed, 51 insertions(+), 2 deletions(-) diff --git a/src/providers/TurnkeyAuthProvider.tsx b/src/providers/TurnkeyAuthProvider.tsx index 8174da4d1..57638a110 100644 --- a/src/providers/TurnkeyAuthProvider.tsx +++ b/src/providers/TurnkeyAuthProvider.tsx @@ -69,7 +69,7 @@ const useTurnkeyAuthContext = () => { const stringGetter = useStringGetter(); const indexerUrl = useAppSelector(selectIndexerUrl); const sourceAccount = useAppSelector(getSourceAccount); - const { indexedDbClient, authIframeClient, passkeyClient } = useTurnkey(); + const { indexedDbClient, authIframeClient, passkeyClient, turnkey } = useTurnkey(); const { dydxAddress: connectedDydxAddress, setWalletFromSignature, selectWallet } = useAccounts(); const [searchParams, setSearchParams] = useSearchParams(); const [emailToken, setEmailToken] = useState(); @@ -572,6 +572,39 @@ const useTurnkeyAuthContext = () => { /* ----------------------------- Passkey Sign In ----------------------------- */ + const registerPasskey = useCallback(async () => { + try { + if (!passkeyClient || !indexedDbClient || !turnkey) { + throw new Error('Passkey client is not available'); + } + + const passkey = await passkeyClient.createUserPasskey({ + rp: { + id: 'dydx.trade', + name: '2FA Passkey', + }, + }); + + const session = await turnkey.getSession(); + if (!session || !passkey?.encodedChallenge || !passkey?.attestation) { + throw new Error('No session found'); + } + + await indexedDbClient.addUserAuth({ + userId: session.userId, + authenticators: [ + { + authenticatorName: 'Passkey', + challenge: passkey.encodedChallenge, + attestation: passkey.attestation, + }, + ], + }); + } catch (error) { + logBonsaiError('TurnkeyOnboarding', 'Error registering passkey', { error }); + } + }, [passkeyClient, indexedDbClient, turnkey]); + const signInWithPasskey = useCallback(async () => { try { if (!passkeyClient) { @@ -682,6 +715,7 @@ const useTurnkeyAuthContext = () => { signInWithOauth, signInWithOtp, signInWithPasskey, + registerPasskey, resetEmailSignInStatus, }; }; diff --git a/src/views/dialogs/EmailSignInStatusDialog.tsx b/src/views/dialogs/EmailSignInStatusDialog.tsx index 67a349126..aee5d70b7 100644 --- a/src/views/dialogs/EmailSignInStatusDialog.tsx +++ b/src/views/dialogs/EmailSignInStatusDialog.tsx @@ -204,7 +204,13 @@ export const EmailSignInStatusDialog = ({ if (showWelcomeContent) { await sleep(0); - dispatch(openDialog(DialogTypes.Deposit2({}))); + dispatch( + openDialog( + DialogTypes.SetupPasskey({ + onClose: () => dispatch(openDialog(DialogTypes.Deposit2({}))), + }) + ) + ); } }} > diff --git a/src/views/dialogs/SetupPasskeyDialog.tsx b/src/views/dialogs/SetupPasskeyDialog.tsx index 90efe8d32..852ae43b5 100644 --- a/src/views/dialogs/SetupPasskeyDialog.tsx +++ b/src/views/dialogs/SetupPasskeyDialog.tsx @@ -2,6 +2,8 @@ import { ReactNode, useCallback, useMemo } from 'react'; import { ButtonAction, ButtonSize, ButtonType } from '@/constants/buttons'; +import { useTurnkeyAuth } from '@/providers/TurnkeyAuthProvider'; + import { Button } from '@/components/Button'; import { Dialog } from '@/components/Dialog'; import { Icon, IconName } from '@/components/Icon'; @@ -13,6 +15,8 @@ export const SetupPasskeyDialog = ({ onClose: () => void; setIsOpen: (isOpen: boolean) => void; }) => { + const { registerPasskey } = useTurnkeyAuth(); + const modifiedSetIsOpen = useCallback( (isOpen: boolean) => { setIsOpen(isOpen); @@ -83,6 +87,10 @@ export const SetupPasskeyDialog = ({ css={{ color: 'var(--dialog-backgroundColor, var(--text-color-text-0))', }} + onClick={() => { + registerPasskey(); + modifiedSetIsOpen(false); + }} > Setup Passkey @@ -93,6 +101,7 @@ export const SetupPasskeyDialog = ({ type={ButtonType.Button} action={ButtonAction.Navigation} size={ButtonSize.BasePlus} + onClick={() => modifiedSetIsOpen(false)} > Skip From 889596ffd972da20a7f2f66cd19f4ba334df38eb Mon Sep 17 00:00:00 2001 From: Kefan Cao Date: Thu, 6 Nov 2025 14:44:48 -0500 Subject: [PATCH 15/21] ff --- src/views/dialogs/EmailSignInStatusDialog.tsx | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/views/dialogs/EmailSignInStatusDialog.tsx b/src/views/dialogs/EmailSignInStatusDialog.tsx index aee5d70b7..64adcc985 100644 --- a/src/views/dialogs/EmailSignInStatusDialog.tsx +++ b/src/views/dialogs/EmailSignInStatusDialog.tsx @@ -22,6 +22,7 @@ import { openDialog } from '@/state/dialogs'; import { getSourceAccount, getTurnkeyEmailOnboardingData } from '@/state/walletSelectors'; import { calc } from '@/lib/do'; +import { testFlags } from '@/lib/testFlags'; import { sleep } from '@/lib/timeUtils'; export const EmailSignInStatusDialog = ({ @@ -204,13 +205,15 @@ export const EmailSignInStatusDialog = ({ if (showWelcomeContent) { await sleep(0); - dispatch( - openDialog( - DialogTypes.SetupPasskey({ - onClose: () => dispatch(openDialog(DialogTypes.Deposit2({}))), - }) - ) - ); + if (testFlags.enablePasskeyAuth) { + dispatch( + openDialog( + DialogTypes.SetupPasskey({ + onClose: () => dispatch(openDialog(DialogTypes.Deposit2({}))), + }) + ) + ); + } } }} > From 0abc2715ea3f1cdb2ef1bf49294d733650c8bc3c Mon Sep 17 00:00:00 2001 From: Jared Vu Date: Thu, 6 Nov 2025 09:03:47 -0800 Subject: [PATCH 16/21] feat: updated fee tiers and staking tiers (#1968) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- package.json | 14 +- pnpm-lock.yaml | 21 +- src/bonsai/calculators/userStats.ts | 24 +- src/bonsai/ontology.ts | 4 +- src/bonsai/selectors/account.ts | 3 +- src/bonsai/types/summaryTypes.ts | 6 + src/components/Separator.tsx | 4 +- src/components/Table.tsx | 361 +++++++++++--------- src/hooks/useStakingTierLevel.ts | 44 +++ src/pages/portfolio/Fees.tsx | 368 --------------------- src/pages/portfolio/Fees/FeePageHeader.tsx | 237 +++++++++++++ src/pages/portfolio/Fees/Fees.tsx | 73 ++++ src/pages/portfolio/Portfolio.tsx | 30 +- src/views/tables/FeeTierTable.tsx | 282 ++++++++++++++++ src/views/tables/FillsTable.tsx | 10 +- src/views/tables/PositionsTable.tsx | 3 - src/views/tables/StakingTierTable.tsx | 225 +++++++++++++ 17 files changed, 1139 insertions(+), 570 deletions(-) create mode 100644 src/hooks/useStakingTierLevel.ts delete mode 100644 src/pages/portfolio/Fees.tsx create mode 100644 src/pages/portfolio/Fees/FeePageHeader.tsx create mode 100644 src/pages/portfolio/Fees/Fees.tsx create mode 100644 src/views/tables/FeeTierTable.tsx create mode 100644 src/views/tables/StakingTierTable.tsx diff --git a/package.json b/package.json index 8c71f8807..30e4d2809 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,7 @@ "@cosmjs/tendermint-rpc": "^0.32.1", "@datadog/browser-logs": "^5.23.3", "@dydxprotocol/v4-client-js": "3.1.1", - "@dydxprotocol/v4-localization": "1.1.349", + "@dydxprotocol/v4-localization": "1.1.351", "@dydxprotocol/v4-proto": "^7.0.0-dev.0", "@emotion/is-prop-valid": "^1.3.0", "@hugocxl/react-to-image": "^0.0.9", @@ -85,10 +85,10 @@ "@radix-ui/react-use-rect": "^1.0.1", "@react-oauth/google": "^0.12.2", "@react-spring/web": "^9.7.2", - "@react-stately/table": "^3.9.1", - "@react-types/grid": "^3.1.8", - "@react-types/shared": "^3.18.1", - "@react-types/table": "^3.6.1", + "@react-stately/table": "^3.15.1", + "@react-types/grid": "^3.3.6", + "@react-types/shared": "^3.32.1", + "@react-types/table": "^3.13.4", "@reduxjs/toolkit": "^2.2.5", "@skip-go/client": "1.5.8", "@solana/web3.js": "^1.93.0", @@ -131,12 +131,12 @@ "mipd": "^0.0.7", "react": "^18.2.0", "react-apple-signin-auth": "^1.1.2", - "react-aria": "^3.25.0", + "react-aria": "^3.44.0", "react-dom": "^18.2.0", "react-number-format": "^5.2.2", "react-redux": "^9.1.2", "react-router-dom": "^6.14.0", - "react-stately": "^3.23.0", + "react-stately": "^3.42.0", "redux-persist": "^6.0.0", "reselect": "^5.1.0", "socket.io-client": "^4.8.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index eff90da1c..b42b68336 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -33,8 +33,8 @@ dependencies: specifier: 3.1.1 version: 3.1.1 '@dydxprotocol/v4-localization': - specifier: 1.1.349 - version: 1.1.349 + specifier: 1.1.351 + version: 1.1.351 '@dydxprotocol/v4-proto': specifier: ^7.0.0-dev.0 version: 7.0.5 @@ -123,16 +123,16 @@ dependencies: specifier: ^9.7.2 version: 9.7.5(react-dom@18.3.1)(react@18.3.1) '@react-stately/table': - specifier: ^3.9.1 + specifier: ^3.15.1 version: 3.15.1(react@18.3.1) '@react-types/grid': - specifier: ^3.1.8 + specifier: ^3.3.6 version: 3.3.6(react@18.3.1) '@react-types/shared': - specifier: ^3.18.1 + specifier: ^3.32.1 version: 3.32.1(react@18.3.1) '@react-types/table': - specifier: ^3.6.1 + specifier: ^3.13.4 version: 3.13.4(react@18.3.1) '@reduxjs/toolkit': specifier: ^2.2.5 @@ -261,7 +261,7 @@ dependencies: specifier: ^1.1.2 version: 1.1.2(react-dom@18.3.1)(react@18.3.1) react-aria: - specifier: ^3.25.0 + specifier: ^3.44.0 version: 3.44.0(react-dom@18.3.1)(react@18.3.1) react-dom: specifier: ^18.2.0 @@ -276,7 +276,7 @@ dependencies: specifier: ^6.14.0 version: 6.30.1(react-dom@18.3.1)(react@18.3.1) react-stately: - specifier: ^3.23.0 + specifier: ^3.42.0 version: 3.42.0(react@18.3.1) redux-persist: specifier: ^6.0.0 @@ -1690,8 +1690,8 @@ packages: - utf-8-validate dev: false - /@dydxprotocol/v4-localization@1.1.349: - resolution: {integrity: sha512-Q3GpEJpG43veWHnQq7y67JoItNRSeFdcEimLFxnWoPCY871449tbFMCUu5ZI26Q1mropZZWxxxd9XNDr8bBTLw==} + /@dydxprotocol/v4-localization@1.1.351: + resolution: {integrity: sha512-jIC9nqtOADQqWd1EGm1C0aWg3OqzuvMjZUUMzZwsjwgMHdOE5YSdRERBUy0Fusyh1GnErBVJb3cITqgEWXtc0Q==} dev: false /@dydxprotocol/v4-proto@7.0.5: @@ -13180,6 +13180,7 @@ packages: /ansi-regex@6.2.2: resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} engines: {node: '>=12'} + requiresBuild: true /ansi-styles@4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} diff --git a/src/bonsai/calculators/userStats.ts b/src/bonsai/calculators/userStats.ts index 02ce17789..a40683245 100644 --- a/src/bonsai/calculators/userStats.ts +++ b/src/bonsai/calculators/userStats.ts @@ -1,8 +1,30 @@ +import { formatUnits } from 'viem/utils'; + import { QUANTUM_MULTIPLIER } from '@/constants/numbers'; import { MustBigNumber } from '@/lib/numbers'; -import { AccountStats, UserFeeTier, UserStakingTier, UserStats } from '../types/summaryTypes'; +import { + AccountStats, + UserFeeTier, + UserStakingTier, + UserStakingTierSummary, + UserStats, +} from '../types/summaryTypes'; + +export const calculateAccountStakingTier = ( + stakingTier: UserStakingTier | undefined +): UserStakingTierSummary | undefined => { + if (!stakingTier) { + return undefined; + } + + return { + feeTierName: stakingTier.feeTierName, + discountPercent: MustBigNumber(stakingTier.discountPpm).div(QUANTUM_MULTIPLIER).toNumber(), + stakedBaseTokens: formatUnits(BigInt(stakingTier.stakedBaseTokens), 18), + }; +}; export function calculateUserStats( feeTier: UserFeeTier | undefined, diff --git a/src/bonsai/ontology.ts b/src/bonsai/ontology.ts index c08897525..dd690cb79 100644 --- a/src/bonsai/ontology.ts +++ b/src/bonsai/ontology.ts @@ -123,7 +123,7 @@ import { SubaccountOrder, SubaccountPosition, SubaccountTransfer, - UserStakingTier, + UserStakingTierSummary, UserStats, } from './types/summaryTypes'; import { useCurrentMarketTradesValue } from './websocket/trades'; @@ -180,7 +180,7 @@ interface BonsaiCoreShape { data: BasicSelector; }; stakingTier: { - data: BasicSelector; + data: BasicSelector; }; }; markets: { diff --git a/src/bonsai/selectors/account.ts b/src/bonsai/selectors/account.ts index 4aa62cf18..d8c8071ba 100644 --- a/src/bonsai/selectors/account.ts +++ b/src/bonsai/selectors/account.ts @@ -27,6 +27,7 @@ import { calculateUnopenedIsolatedPositions, } from '../calculators/subaccount'; import { calculateTransfers } from '../calculators/transfers'; +import { calculateAccountStakingTier } from '../calculators/userStats'; import { mergeLoadableStatus } from '../lib/mapLoadable'; import { selectParentSubaccountInfo } from '../socketSelectors'; import { SubaccountTransfer } from '../types/summaryTypes'; @@ -275,5 +276,5 @@ export const selectAccountNobleWalletAddress = createAppSelector( export const selectAccountStakingTier = createAppSelector( [selectRawAccountStakingTierData], - (stakingTier) => stakingTier + (stakingTier) => calculateAccountStakingTier(stakingTier) ); diff --git a/src/bonsai/types/summaryTypes.ts b/src/bonsai/types/summaryTypes.ts index ca85679bf..6aff020ab 100644 --- a/src/bonsai/types/summaryTypes.ts +++ b/src/bonsai/types/summaryTypes.ts @@ -279,6 +279,12 @@ export type PerpetualMarketFeeDiscount = NonNullable< export type UserFeeTier = NonNullable>; export type UserStakingTier = NonNullable>; +export interface UserStakingTierSummary { + feeTierName: string; + discountPercent: number | undefined; + stakedBaseTokens: string | undefined; +} + export type EquityTiers = NonNullable< ToPrimitives >; diff --git a/src/components/Separator.tsx b/src/components/Separator.tsx index 3efb846df..caafbbf02 100644 --- a/src/components/Separator.tsx +++ b/src/components/Separator.tsx @@ -4,8 +4,6 @@ import { Separator } from '@radix-ui/react-separator'; import styled, { css } from 'styled-components'; const StyledSeparator = styled(Separator)<{ $fullHeight: boolean }>` - --separatorHeight-padding: 1.5rem; - flex: 0 !important; z-index: -1; @@ -28,7 +26,7 @@ const StyledSeparator = styled(Separator)<{ $fullHeight: boolean }>` height: 100%; ` : css` - height: calc(100% - var(--separatorHeight-padding)); + height: calc(100% - var(--separatorHeight-padding, 1.5rem)); `} } `; diff --git a/src/components/Table.tsx b/src/components/Table.tsx index 15a46a465..99d3ced66 100644 --- a/src/components/Table.tsx +++ b/src/components/Table.tsx @@ -13,7 +13,7 @@ import { import { type GridNode } from '@react-types/grid'; import type { Node, SortDescriptor, SortDirection } from '@react-types/shared'; import { type ColumnSize, type TableCollection } from '@react-types/table'; -import { isFunction } from 'lodash'; +import { flatMap, isFunction } from 'lodash'; import { mergeProps, useCollator, @@ -30,10 +30,10 @@ import styled, { css } from 'styled-components'; import { MediaQueryKeys, useBreakpoints } from '@/hooks/useBreakpoints'; import { useTablePagination } from '@/hooks/useTablePagination'; -import breakpoints from '@/styles/breakpoints'; import { layoutMixins } from '@/styles/layoutMixins'; import { MustBigNumber } from '@/lib/numbers'; +import { objectFromEntries } from '@/lib/objectHelpers'; import { SortIcon } from './SortIcon'; import { PAGE_SIZES, PageSize, TablePaginationRow } from './Table/TablePaginationRow'; @@ -82,12 +82,13 @@ export type SelectedKey = 'all' | Iterable | undefined; export type ColumnDef = { columnKey: string; - label: React.ReactNode; + label: NonNullable; tag?: React.ReactNode; colspan?: number; childColumns?: ColumnDef[]; + isRowHeader?: boolean; allowsResizing?: boolean; - renderCell: (row: TableRowData) => React.ReactNode; + renderCell?: (row: TableRowData) => React.ReactNode; isActionable?: boolean; hideOnBreakpoint?: MediaQueryKeys; width?: ColumnSize; @@ -115,7 +116,6 @@ export type TableElementProps({ initialPageSize = 10, paginationBehavior = 'paginate', hideHeader = false, - withGradientCardRows = false, withFocusStickyRows = false, withOuterBorder = false, withInnerBorders = false, @@ -248,12 +247,60 @@ export const Table = ({ : items, [currentPage, items, pageSize, shouldPaginate] ); + + const renderCell = useCallback( + ( + item: TableRowData | CustomRowConfig, + columnKey: string | number, + allColumns: ColumnDef[] + ) => { + if (isTableRowData(item)) { + const columnMapByKey = objectFromEntries( + [...allColumns, ...flatMap(allColumns, (column) => column.childColumns ?? [])].map( + (column) => [column.columnKey, column] + ) + ); + + const maybeRenderColumn = columnMapByKey[columnKey]; + return maybeRenderColumn?.renderCell?.(item) ?? null; + } + + return null; + }, + [] + ); + + /** + * | column header 1 | column header 2 | + * | header A | header B | Header C | + * @returns column defs when nested columns are considered and whether the columnDefs are nested + */ + const { leafColumns, hasNestedColumns } = useMemo(() => { + const allColumns: ColumnDef[] = []; + let hasChildColumns: boolean = false; + + const findAndAppendColumns = (cols: ColumnDef[]) => { + cols.forEach((c) => { + if (c.childColumns?.length) { + hasChildColumns = true; + findAndAppendColumns(c.childColumns); + } else { + allColumns.push(c); + } + }); + }; + findAndAppendColumns(shownColumns); + return { + leafColumns: allColumns, + hasNestedColumns: hasChildColumns, + }; + }, [shownColumns]); + return ( <$TableWrapper className={className} style={style} isEmpty={isEmpty} - withGradientCardRows={withGradientCardRows} withOuterBorder={withOuterBorder} > {!isEmpty ? ( @@ -275,13 +322,13 @@ export const Table = ({ )) } hideHeader={hideHeader} - withGradientCardRows={withGradientCardRows} withFocusStickyRows={withFocusStickyRows} withOuterBorder={withOuterBorder} withInnerBorders={withInnerBorders} withScrollSnapColumns={withScrollSnapColumns} withScrollSnapRows={withScrollSnapRows} - numColumns={shownColumns.length} + numColumns={leafColumns.length} + hasNestedColumns={hasNestedColumns} firstClickSortDirection={firstClickSortDirection} paginationRow={ shouldPaginate ? ( @@ -299,11 +346,12 @@ export const Table = ({ {(column) => ( {column.label} {column.tag && {column.tag}} @@ -312,16 +360,17 @@ export const Table = ({ - {(item) => ( - - {(columnKey) => ( - - {isTableRowData(item) && - columns.find((column) => column.columnKey === columnKey)?.renderCell(item)} - - )} - - )} + {(item) => { + return ( + + {(columnKey) => ( + + {renderCell(item, columnKey, columns)} + + )} + + ); + }} ) : ( @@ -350,8 +399,8 @@ const TableRoot = (prop paginationRow?: React.ReactNode; firstClickSortDirection?: 'ascending' | 'descending'; + hasNestedColumns?: boolean; hideHeader?: boolean; - withGradientCardRows?: boolean; withFocusStickyRows?: boolean; withOuterBorder?: boolean; withInnerBorders?: boolean; @@ -366,8 +415,8 @@ const TableRoot = (prop onRowAction, numColumns, paginationRow, + hasNestedColumns, hideHeader, - withGradientCardRows, withFocusStickyRows, withOuterBorder, withInnerBorders, @@ -406,46 +455,34 @@ const TableRoot = (prop ref ); + const rows = React.useMemo( + () => Array.from(collection.body.childNodes), + [collection.body.childNodes] + ); + return ( <$Table ref={ref} {...gridProps} hideHeader={hideHeader} - withGradientCardRows={withGradientCardRows} withOuterBorder={withOuterBorder} withInnerBorders={withInnerBorders} > -