diff --git a/config.devnet.json b/config.devnet.json index bf038547..5e908bf6 100644 --- a/config.devnet.json +++ b/config.devnet.json @@ -12,6 +12,16 @@ "rippleIssuerAddress": "rLTBw1MEy45uE5qmkWseinbj7h4zmdQuR8", "xrplWebsocket": "wss://s.altnet.rippletest.net:51233", "ledgerApp": "Bitcoin Test", + "dfnsConfiguration": { + "dfnsBaseURL": "https://api.dfns.ninja", + "dfnsCustomerConfigurations": [ + { + "name": "Tungsten", + "organizationID": "or-3pqgf-ugmhq-8969lp77c7f8uf81", + "applicationID": "ap-513sv-f5knb-811qggt4oh6hd5s9" + } + ] + }, "merchants": [ { "name": "Amber", diff --git a/config.mainnet.json b/config.mainnet.json index ecb72180..8e10ac67 100644 --- a/config.mainnet.json +++ b/config.mainnet.json @@ -12,6 +12,16 @@ "rippleIssuerAddress": "rGcyRGrZPaJAZbZDi4NqRFLA5GQH63iFpD", "xrplWebsocket": "wss://xrpl.ws/", "ledgerApp": "Bitcoin", + "dfnsConfiguration": { + "dfnsBaseURL": "https://api.dfns.ninja", + "dfnsCustomerConfigurations": [ + { + "name": "Tungsten", + "organizationID": "or-3pqgf-ugmhq-8969lp77c7f8uf81", + "applicationID": "ap-513sv-f5knb-811qggt4oh6hd5s9" + } + ] + }, "merchants": [ { "name": "Amber", diff --git a/config.testnet.json b/config.testnet.json index 5e039a57..fdd81e6e 100644 --- a/config.testnet.json +++ b/config.testnet.json @@ -12,6 +12,16 @@ "rippleIssuerAddress": "ra3oyRVfy4yD4NJPrVcewvDtisZ3FhkcYL", "xrplWebsocket": "wss://testnet.xrpl-labs.com/", "ledgerApp": "Bitcoin Test", + "dfnsConfiguration": { + "dfnsBaseURL": "https://api.dfns.ninja", + "dfnsCustomerConfigurations": [ + { + "name": "Tungsten", + "organizationID": "or-3pqgf-ugmhq-8969lp77c7f8uf81", + "applicationID": "ap-513sv-f5knb-811qggt4oh6hd5s9" + } + ] + }, "merchants": [ { "name": "Amber", diff --git a/netlify/functions/get-dfns-auth-token.ts b/netlify/functions/get-dfns-auth-token.ts new file mode 100644 index 00000000..4f6c692c --- /dev/null +++ b/netlify/functions/get-dfns-auth-token.ts @@ -0,0 +1,38 @@ +import { DfnsAuthenticator } from '@dfns/sdk'; +import { WebAuthnSigner } from '@dfns/sdk-browser'; +import { Handler } from '@netlify/functions'; + +const handler: Handler = async event => { + try { + const dfnsAuth = new DfnsAuthenticator({ + appId: 'ap-7perp-pnqr6-8f5pgmmr6dtgh4n3', + baseUrl: 'https://app.dfns.ninja', + signer: new WebAuthnSigner(), + }); + + const { token } = await dfnsAuth.login({ + username: 'us-12nks-o4ogd-943qhq5l5020410m', + orgId: 'or-4bcun-fj3tb-8np9ivfu5jnstkks', + }); + + console.log( + 'tokeEEEEEEEEEEEEEEEEEEEEEEEEEEnEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE', + token + ); + + return { + statusCode: 200, + body: token, + }; + } catch (error) { + console.error(error); + return { + statusCode: 500, + body: JSON.stringify({ + error: error.message, + }), + }; + } +}; + +export { handler }; diff --git a/netlify/functions/get-wallets.ts b/netlify/functions/get-wallets.ts new file mode 100644 index 00000000..db688888 --- /dev/null +++ b/netlify/functions/get-wallets.ts @@ -0,0 +1,34 @@ +import { DfnsAuthenticator, DfnsDelegatedApiClient } from '@dfns/sdk'; +import { WebAuthnSigner } from '@dfns/sdk-browser'; +import { Handler } from '@netlify/functions'; + +const handler: Handler = async event => { + try { + const dfnsDelegated = new DfnsDelegatedApiClient({ + baseUrl: 'https://app.dfns.ninja', + appId: 'ap-7perp-pnqr6-8f5pgmmr6dtgh4n3', // ID of the Application registered with DFNS + authToken: + 'eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSJ9.eyJpc3MiOiJhdXRoLmRmbnMubmluamEiLCJhdWQiOiJkZm5zOmF1dGg6dXNlciIsInN1YiI6Im9yLTRiY3VuLWZqM3RiLThucDlpdmZ1NWpuc3Rra3MiLCJqdGkiOiJ1ai0xNDJmay02aHZvZi05M2Q5cGY3NzE3cm1sZjYxIiwic2NvcGUiOiIiLCJwZXJtaXNzaW9ucyI6WyJBdXRoOkFjdGlvbjpTaWduIiwiQXV0aDpBcHBzOkNyZWF0ZSIsIkF1dGg6QXBwczpSZWFkIiwiQXV0aDpBcHBzOlVwZGF0ZSIsIkF1dGg6Q3JlZHM6Q3JlYXRlIiwiQXV0aDpDcmVkczpSZWFkIiwiQXV0aDpDcmVkczpVcGRhdGUiLCJBdXRoOkNyZWRzOkNvZGU6Q3JlYXRlIiwiQXV0aDpUeXBlczpBcHBsaWNhdGlvbiIsIkF1dGg6VHlwZXM6RW1wbG95ZWUiLCJBdXRoOlR5cGVzOkVuZFVzZXIiLCJBdXRoOlR5cGVzOlBhdCIsIkF1dGg6VHlwZXM6U2VydmljZUFjY291bnQiLCJBdXRoOlVzZXJzOkNyZWF0ZSIsIkF1dGg6VXNlcnM6RGVsZWdhdGUiLCJBdXRoOlVzZXJzOlJlYWQiLCJBdXRoOlVzZXJzOlVwZGF0ZSIsIkV4Y2hhbmdlczpDcmVhdGUiLCJFeGNoYW5nZXM6UmVhZCIsIkV4Y2hhbmdlczpEZWxldGUiLCJFeGNoYW5nZXM6RGVwb3NpdHM6Q3JlYXRlIiwiRXhjaGFuZ2VzOldpdGhkcmF3YWxzOkNyZWF0ZSIsIk9yZ3M6UmVhZCIsIk9yZ3M6VXBkYXRlIiwiT3JnczpTZXR0aW5nczpSZWFkIiwiT3JnczpTZXR0aW5nczpVcGRhdGUiLCJQZXJtaXNzaW9uQXNzaWdubWVudHM6Q3JlYXRlIiwiUGVybWlzc2lvbkFzc2lnbm1lbnRzOlJlYWQiLCJQZXJtaXNzaW9uQXNzaWdubWVudHM6UmV2b2tlIiwiUGVybWlzc2lvblByZWRpY2F0ZXM6QXJjaGl2ZSIsIlBlcm1pc3Npb25QcmVkaWNhdGVzOkNyZWF0ZSIsIlBlcm1pc3Npb25QcmVkaWNhdGVzOlJlYWQiLCJQZXJtaXNzaW9uUHJlZGljYXRlczpVcGRhdGUiLCJQZXJtaXNzaW9uczpBcmNoaXZlIiwiUGVybWlzc2lvbnM6Q3JlYXRlIiwiUGVybWlzc2lvbnM6UmVhZCIsIlBlcm1pc3Npb25zOlVwZGF0ZSIsIlBvbGljaWVzOkFyY2hpdmUiLCJQb2xpY2llczpDcmVhdGUiLCJQb2xpY2llczpSZWFkIiwiUG9saWNpZXM6VXBkYXRlIiwiUG9saWNpZXM6QXBwcm92YWxzOlJlYWQiLCJQb2xpY2llczpBcHByb3ZhbHM6QXBwcm92ZSIsIlNpZ25lcnM6TGlzdFNpZ25lcnMiLCJXYWxsZXRzOkJyb2FkY2FzdFRyYW5zYWN0aW9uIiwiV2FsbGV0czpDcmVhdGUiLCJXYWxsZXRzOkRlbGVnYXRlIiwiV2FsbGV0czpFeHBvcnQiLCJXYWxsZXRzOkdlbmVyYXRlU2lnbmF0dXJlIiwiV2FsbGV0czpJbXBvcnQiLCJXYWxsZXRzOlJlYWQiLCJXYWxsZXRzOlJlYWRTaWduYXR1cmUiLCJXYWxsZXRzOlJlYWRUcmFuc2FjdGlvbiIsIldhbGxldHM6UmVhZFRyYW5zZmVyIiwiV2FsbGV0czpUcmFuc2ZlckFzc2V0IiwiV2FsbGV0czpVcGRhdGUiLCJXYWxsZXRzOlRhZ3M6QWRkIiwiV2FsbGV0czpUYWdzOkRlbGV0ZSIsIldlYmhvb2tzOkNyZWF0ZSIsIldlYmhvb2tzOlJlYWQiLCJXZWJob29rczpVcGRhdGUiLCJXZWJob29rczpEZWxldGUiLCJXZWJob29rczpQaW5nIiwiV2ViaG9va3M6RXZlbnRzOlJlYWQiXSwiaHR0cHM6Ly9jdXN0b20vdXNlcm5hbWUiOiJkYW5pQGRsYy5saW5rIiwiaHR0cHM6Ly9jdXN0b20vYXBwX21ldGFkYXRhIjp7InVzZXJJZCI6InVzLTEybmtzLW80b2dkLTk0M3FocTVsNTAyMDQxMG0iLCJvcmdJZCI6Im9yLTRiY3VuLWZqM3RiLThucDlpdmZ1NWpuc3Rra3MiLCJ0b2tlbktpbmQiOiJUb2tlbiJ9LCJpYXQiOjE3MzA3OTY3MDUsImV4cCI6MTczMDgxODMwNX0.MplBRIoMls9sxUakQn1bXfwXxOK6u4TGosoOYlREY5oX4A_7VInLzfQ9-uU8P_TAgX-lnf9nCH6_UtjDTqC9Bg', // Auth token of the User + }); + + console.log('dfnsDelegated', dfnsDelegated); + + const wallets = await dfnsDelegated.wallets.listWallets(); + console.log('wallets', wallets); + + return { + statusCode: 200, + body: JSON.stringify(wallets), + }; + } catch (error) { + console.error(error); + return { + statusCode: 500, + body: JSON.stringify({ + error: error.message, + }), + }; + } +}; + +export { handler }; diff --git a/package.json b/package.json index 2d934487..4251a8b7 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,8 @@ "dependencies": { "@chakra-ui/icons": "^2.1.1", "@chakra-ui/react": "^2.8.2", + "@dfns/sdk": "^0.5.9", + "@dfns/sdk-browser": "^0.5.9", "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.5", "@fontsource-variable/onest": "^5.0.3", @@ -46,7 +48,7 @@ "concurrently": "^8.2.2", "d3": "^7.9.0", "decimal.js": "^10.4.3", - "dlc-btc-lib": "2.4.18", + "dlc-btc-lib": "2.4.19-dfns-beta", "dotenv": "^16.3.1", "ethers": "5.7.2", "formik": "^2.4.5", diff --git a/public/images/logos/dfns-logo.svg b/public/images/logos/dfns-logo.svg new file mode 100644 index 00000000..0cbb18da --- /dev/null +++ b/public/images/logos/dfns-logo.svg @@ -0,0 +1 @@ + diff --git a/src/app/components/modals/components/modal-container.tsx b/src/app/components/modals/components/modal-container.tsx index 84b71b2e..53acc5be 100644 --- a/src/app/components/modals/components/modal-container.tsx +++ b/src/app/components/modals/components/modal-container.tsx @@ -5,6 +5,7 @@ import { AnyAction } from '@reduxjs/toolkit'; import { RootState } from '@store/index'; import { modalActions } from '@store/slices/modal/modal.actions'; +import { DFNSModal } from '../dfns-modal/dfns-modal'; import { LedgerModal } from '../ledger-modal/ledger-modal'; import { SelectBitcoinWalletModal } from '../select-bitcoin-wallet-modal/select-bitcoin-wallet-modal'; import { SuccessfulFlowModal } from '../successful-flow-modal/successful-flow-modal'; @@ -21,6 +22,7 @@ export function ModalContainer(): React.JSX.Element { isSuccesfulFlowModalOpen, isSelectBitcoinWalletModalOpen, isLedgerModalOpen, + isDFNSModalOpen, } = useSelector((state: RootState) => state.modal); const handleClosingModal = (actionCreator: () => AnyAction) => { @@ -60,6 +62,10 @@ export function ModalContainer(): React.JSX.Element { isOpen={isLedgerModalOpen} handleClose={() => handleClosingModal(modalActions.toggleLedgerModalVisibility)} /> + handleClosingModal(modalActions.toggleDFNSModalVisibility)} + /> ); } diff --git a/src/app/components/modals/dfns-modal/components/dfns-modal-credentials-form.tsx b/src/app/components/modals/dfns-modal/components/dfns-modal-credentials-form.tsx new file mode 100644 index 00000000..20dbe82a --- /dev/null +++ b/src/app/components/modals/dfns-modal/components/dfns-modal-credentials-form.tsx @@ -0,0 +1,72 @@ +import { Button, FormControl, FormErrorMessage, FormLabel, Input, VStack } from '@chakra-ui/react'; +import { DFNSCustomerConfiguration } from '@models/configuration'; +import { useForm } from '@tanstack/react-form'; + +interface DFNSModalRegisterFormProps { + onSubmit: ( + credentialCode: string, + selectedDFNSOrganization: DFNSCustomerConfiguration + ) => Promise; + selectedDFNSOrganization: DFNSCustomerConfiguration; +} + +export function DFNSModalRegisterForm({ + onSubmit, + selectedDFNSOrganization, +}: DFNSModalRegisterFormProps): React.JSX.Element { + const formAPI = useForm({ + defaultValues: { + credentialCode: '', + }, + onSubmit: async ({ value }) => { + await onSubmit(value.credentialCode, selectedDFNSOrganization); + }, + }); + + return ( + +
{ + e.preventDefault(); + e.stopPropagation(); + void formAPI.handleSubmit(); + }} + > + + { + if (!value) return 'Credential Code is required'; + if (value.length < 9) return 'Credential Code must be at least 9 characters'; + return undefined; + }, + }} + children={field => ( + + + Credential Code + + field.handleChange(e.target.value)} + placeholder="xxxxxxxxx" + /> + {field.state.meta.errorMap.onChange} + + )} + /> + [state.canSubmit]} + children={([canSubmit]) => ( + + )} + /> + +
+
+ ); +} diff --git a/src/app/components/modals/dfns-modal/components/dfns-modal-error-box.tsx b/src/app/components/modals/dfns-modal/components/dfns-modal-error-box.tsx new file mode 100644 index 00000000..24ba22a4 --- /dev/null +++ b/src/app/components/modals/dfns-modal/components/dfns-modal-error-box.tsx @@ -0,0 +1,25 @@ +import { HStack, Text } from '@chakra-ui/react'; + +interface DFNSModalErrorBoxProps { + error: string | undefined; +} + +export function DFNSModalErrorBox({ error }: DFNSModalErrorBoxProps): React.JSX.Element | false { + return ( + !!error && ( + + + {error} + + + ) + ); +} diff --git a/src/app/components/modals/dfns-modal/components/dfns-modal-form.tsx b/src/app/components/modals/dfns-modal/components/dfns-modal-form.tsx new file mode 100644 index 00000000..80713c9b --- /dev/null +++ b/src/app/components/modals/dfns-modal/components/dfns-modal-form.tsx @@ -0,0 +1,78 @@ +import { Button, FormControl, FormErrorMessage, FormLabel, Input, VStack } from '@chakra-ui/react'; +import { DFNSCustomerConfiguration } from '@models/configuration'; +import { useForm } from '@tanstack/react-form'; + +interface DFNSModalLoginFormProps { + onSubmit: (email: string, selectedDFNSOrganization: DFNSCustomerConfiguration) => Promise; + selectedDFNSOrganization: DFNSCustomerConfiguration; +} + +const validateEmail = (email: string) => { + if (!email) return 'Email address is required'; + + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(email)) return 'Please enter a valid email address'; + + const [localPart] = email.split('@'); + if (localPart.length > 64) return 'Local part of email cannot exceed 64 characters'; + if (email.length > 254) return 'Email address cannot exceed 254 characters'; + + return undefined; +}; + +export function DFNSModalLoginForm({ + onSubmit, + selectedDFNSOrganization, +}: DFNSModalLoginFormProps): React.JSX.Element { + const formAPI = useForm({ + defaultValues: { + email: '', + }, + onSubmit: async ({ value }) => { + await onSubmit(value.email, selectedDFNSOrganization); + }, + }); + + return ( + +
{ + e.preventDefault(); + e.stopPropagation(); + void formAPI.handleSubmit(); + }} + > + + validateEmail(value), + }} + children={field => ( + + + E-Mail Address + + field.handleChange(e.target.value)} + placeholder="example@domain.com" + /> + {field.state.meta.errorMap.onChange} + + )} + /> + [state.canSubmit]} + children={([canSubmit]) => ( + + )} + /> + +
+
+ ); +} diff --git a/src/app/components/modals/dfns-modal/components/dfns-modal-layout.tsx b/src/app/components/modals/dfns-modal/components/dfns-modal-layout.tsx new file mode 100644 index 00000000..380cc2bf --- /dev/null +++ b/src/app/components/modals/dfns-modal/components/dfns-modal-layout.tsx @@ -0,0 +1,43 @@ +import { ReactNode } from 'react'; + +import { + Image, + Modal, + ModalBody, + ModalCloseButton, + ModalContent, + ModalHeader, + ModalOverlay, + VStack, +} from '@chakra-ui/react'; + +interface DFNSModalLayoutProps { + logo: string; + isOpen: boolean; + onClose: () => void; + children: ReactNode; +} + +export function DFNSModalLayout({ + logo, + isOpen, + onClose, + children, +}: DFNSModalLayoutProps): React.JSX.Element { + return ( + + + + + {'DFNS + + + + + {children} + + + + + ); +} diff --git a/src/app/components/modals/dfns-modal/components/dfns-modal-loading-stack.tsx b/src/app/components/modals/dfns-modal/components/dfns-modal-loading-stack.tsx new file mode 100644 index 00000000..95dc93be --- /dev/null +++ b/src/app/components/modals/dfns-modal/components/dfns-modal-loading-stack.tsx @@ -0,0 +1,19 @@ +import { HStack, Spinner, Text } from '@chakra-ui/react'; + +interface DFNSModalLoadingStackProps { + isLoading: [boolean, string]; +} +export function DFNSModalLoadingStack({ + isLoading, +}: DFNSModalLoadingStackProps): React.JSX.Element | false { + return ( + isLoading[0] && ( + + + {isLoading[1]} + + + + ) + ); +} diff --git a/src/app/components/modals/dfns-modal/components/dfns-modal-navigator-button-stack.tsx b/src/app/components/modals/dfns-modal/components/dfns-modal-navigator-button-stack.tsx new file mode 100644 index 00000000..d7fb2d38 --- /dev/null +++ b/src/app/components/modals/dfns-modal/components/dfns-modal-navigator-button-stack.tsx @@ -0,0 +1,28 @@ +import { Button, Text, VStack } from '@chakra-ui/react'; + +interface DFNSModalNavigatorButtonProps { + isRegister: boolean; + setIsRegister: (isRegister: boolean) => void; + isVisible: boolean; +} + +export function DFNSModalNavigatorButton({ + isRegister, + setIsRegister, + isVisible, +}: DFNSModalNavigatorButtonProps): React.JSX.Element | false { + if (!isVisible) return false; + + return ( + + + {!isRegister + ? 'New user? Click the button below to register with your DFNS credential code.' + : 'Existing user? Sign in with your e-mail address.'} + + + + ); +} diff --git a/src/app/components/modals/dfns-modal/components/dfns-modal-select-address-menu/components/dfns-modal-address-button.tsx b/src/app/components/modals/dfns-modal/components/dfns-modal-select-address-menu/components/dfns-modal-address-button.tsx new file mode 100644 index 00000000..1bf61e6f --- /dev/null +++ b/src/app/components/modals/dfns-modal/components/dfns-modal-select-address-menu/components/dfns-modal-address-button.tsx @@ -0,0 +1,23 @@ +import { Button, HStack, Text } from '@chakra-ui/react'; +import { truncateAddress } from 'dlc-btc-lib/utilities'; + +interface DFNSModalAddressButtonProps { + addressInformation: { address: string | undefined; walletID: string }; + setWalletID: (walletID: string) => void; +} + +export function DFNSModalAddressButton({ + addressInformation, + setWalletID, +}: DFNSModalAddressButtonProps): React.JSX.Element { + const { address, walletID } = addressInformation; + return ( + + ); +} diff --git a/src/app/components/modals/dfns-modal/components/dfns-modal-select-address-menu/dfns-modal-select-address-menu.tsx b/src/app/components/modals/dfns-modal/components/dfns-modal-select-address-menu/dfns-modal-select-address-menu.tsx new file mode 100644 index 00000000..4b461a98 --- /dev/null +++ b/src/app/components/modals/dfns-modal/components/dfns-modal-select-address-menu/dfns-modal-select-address-menu.tsx @@ -0,0 +1,46 @@ +import { ButtonGroup, Text, VStack } from '@chakra-ui/react'; +import { scrollBarDFNSAddressCSS } from '@styles/css-styles'; + +import { DFNSModalAddressButton } from './components/dfns-modal-address-button'; + +interface DFNSModalSelectAddressMenuProps { + taprootAddresses: { address: string | undefined; walletID: string }[] | undefined; + isLoading: boolean; + isSuccesful: boolean; + error: string | undefined; + setWalletID: (walletID: string) => void; +} + +export function DFNSModalSelectAddressMenu({ + taprootAddresses, + isLoading, + isSuccesful, + error, + setWalletID, +}: DFNSModalSelectAddressMenuProps): React.JSX.Element | false { + return ( + !isLoading && + !isSuccesful && + !error && ( + + Select Bitcoin Address + + {taprootAddresses?.map(address => ( + + ))} + + + ) + ); +} diff --git a/src/app/components/modals/dfns-modal/components/dfns-modal-select-organization-menu.tsx b/src/app/components/modals/dfns-modal/components/dfns-modal-select-organization-menu.tsx new file mode 100644 index 00000000..1f7b73b0 --- /dev/null +++ b/src/app/components/modals/dfns-modal/components/dfns-modal-select-organization-menu.tsx @@ -0,0 +1,44 @@ +import { ChevronDownIcon } from '@chakra-ui/icons'; +import { HStack, Menu, MenuButton, MenuItem, MenuList, Text, VStack } from '@chakra-ui/react'; +import { DFNSCustomerConfiguration } from '@models/configuration'; + +interface DFNSModalSelectOrganizationMenuProps { + handleChangeOrganization: (dfnsOrganization: DFNSCustomerConfiguration) => void; + dfnsOrganizations: DFNSCustomerConfiguration[]; + selectedDFNSOrganization: DFNSCustomerConfiguration; +} + +export function DFNSModalSelectOrganizationMenu({ + handleChangeOrganization, + dfnsOrganizations, + selectedDFNSOrganization, +}: DFNSModalSelectOrganizationMenuProps): React.JSX.Element { + return ( + + + Organization + + + + + {selectedDFNSOrganization.name} + + + + + {dfnsOrganizations.map(dfnsOrganization => { + return ( + handleChangeOrganization(dfnsOrganization)} + > + {dfnsOrganization.name} + + ); + })} + + + + ); +} diff --git a/src/app/components/modals/dfns-modal/components/dfns-modal-success-icon.tsx b/src/app/components/modals/dfns-modal/components/dfns-modal-success-icon.tsx new file mode 100644 index 00000000..03b65be5 --- /dev/null +++ b/src/app/components/modals/dfns-modal/components/dfns-modal-success-icon.tsx @@ -0,0 +1,16 @@ +import { CheckCircleIcon } from '@chakra-ui/icons'; +import { SlideFade } from '@chakra-ui/react'; + +interface DFNSModalSuccessIconProps { + isSuccesful: boolean; +} + +export function DFNSModalSuccessIcon({ + isSuccesful, +}: DFNSModalSuccessIconProps): React.JSX.Element { + return ( + + + + ); +} diff --git a/src/app/components/modals/dfns-modal/dfns-modal.tsx b/src/app/components/modals/dfns-modal/dfns-modal.tsx new file mode 100644 index 00000000..6e02cad3 --- /dev/null +++ b/src/app/components/modals/dfns-modal/dfns-modal.tsx @@ -0,0 +1,127 @@ +import { useContext, useState } from 'react'; + +import { Collapse, VStack } from '@chakra-ui/react'; +import { useDFNS } from '@hooks/use-dfns'; +import { DFNSCustomerConfiguration } from '@models/configuration'; +import { + BitcoinWalletContext, + BitcoinWalletContextState, +} from '@providers/bitcoin-wallet-context-provider'; +import { delay } from 'dlc-btc-lib/utilities'; + +import { ModalComponentProps } from '../components/modal-container'; +import { DFNSModalRegisterForm } from './components/dfns-modal-credentials-form'; +import { DFNSModalErrorBox } from './components/dfns-modal-error-box'; +import { DFNSModalLoginForm } from './components/dfns-modal-form'; +import { DFNSModalLayout } from './components/dfns-modal-layout'; +import { DFNSModalLoadingStack } from './components/dfns-modal-loading-stack'; +import { DFNSModalNavigatorButton } from './components/dfns-modal-navigator-button-stack'; +import { DFNSModalSelectAddressMenu } from './components/dfns-modal-select-address-menu/dfns-modal-select-address-menu'; +import { DFNSModalSelectOrganizationMenu } from './components/dfns-modal-select-organization-menu'; +import { DFNSModalSuccessIcon } from './components/dfns-modal-success-icon'; + +export function DFNSModal({ isOpen, handleClose }: ModalComponentProps): React.JSX.Element { + const { connectDFNSWallet, registerCredentials, selectWallet, isLoading } = useDFNS(); + + const { setBitcoinWalletContextState } = useContext(BitcoinWalletContext); + + const [taprootAddresses, setTaprootAddresses] = useState< + { address: string | undefined; walletID: string }[] | undefined + >(undefined); + const [organization, setOrganization] = useState( + appConfiguration.dfnsConfiguration.dfnsCustomerConfigurations[0] + ); + + const [isSuccesful, setIsSuccesful] = useState(false); + const [isRegister, setIsRegister] = useState(false); + const [isLoadingAddressList, setIsLoadingAddressList] = useState(true); + const [dfnsError, setDFNSError] = useState(undefined); + + function resetDFNSModalValues() { + setIsSuccesful(false); + setTaprootAddresses(undefined); + setIsLoadingAddressList(true); + setDFNSError(undefined); + } + + async function setError(error: string) { + setDFNSError(error); + await delay(5000); + setDFNSError(undefined); + } + + async function connectDFNSWalletAndGetAddresses( + username: string, + organization: DFNSCustomerConfiguration + ) { + try { + const taprootAddresses = await connectDFNSWallet(username, organization); + setTaprootAddresses(taprootAddresses); + setIsLoadingAddressList(false); + } catch (error: any) { + await setError(error.message); + } + } + + async function handleRegisterCredentials(code: string, organization: DFNSCustomerConfiguration) { + try { + await registerCredentials(code, organization); + setIsRegister(false); + } catch (error: any) { + await setError(error.message); + } + } + + async function setWalletID(walletID: string) { + try { + await selectWallet(walletID); + setIsSuccesful(true); + await delay(2500); + resetDFNSModalValues(); + setBitcoinWalletContextState(BitcoinWalletContextState.READY); + handleClose(); + } catch (error: any) { + await setError(error.message); + } + } + + return ( + + + + + + {!isRegister ? ( + + ) : ( + + )} + + + + + + + + ); +} diff --git a/src/app/components/modals/select-bitcoin-wallet-modal/components/select-bitcoin-wallet-modal-menu.tsx b/src/app/components/modals/select-bitcoin-wallet-modal/components/select-bitcoin-wallet-modal-menu.tsx index a8c78ea3..2047c65b 100644 --- a/src/app/components/modals/select-bitcoin-wallet-modal/components/select-bitcoin-wallet-modal-menu.tsx +++ b/src/app/components/modals/select-bitcoin-wallet-modal/components/select-bitcoin-wallet-modal-menu.tsx @@ -15,7 +15,7 @@ export function SelectBitcoinWalletMenu({ return ( diff --git a/src/app/components/modals/select-bitcoin-wallet-modal/select-bitcoin-wallet-modal.tsx b/src/app/components/modals/select-bitcoin-wallet-modal/select-bitcoin-wallet-modal.tsx index 6e7899db..64c7d8b4 100644 --- a/src/app/components/modals/select-bitcoin-wallet-modal/select-bitcoin-wallet-modal.tsx +++ b/src/app/components/modals/select-bitcoin-wallet-modal/select-bitcoin-wallet-modal.tsx @@ -64,6 +64,10 @@ export function SelectBitcoinWalletModal({ case BitcoinWalletType.Ledger: dispatch(modalActions.toggleLedgerModalVisibility()); break; + case BitcoinWalletType.DFNS: + dispatch(modalActions.toggleDFNSModalVisibility()); + break; + break; default: break; } diff --git a/src/app/hooks/use-dfns.ts b/src/app/hooks/use-dfns.ts new file mode 100644 index 00000000..9b3c92b1 --- /dev/null +++ b/src/app/hooks/use-dfns.ts @@ -0,0 +1,272 @@ +import { useContext, useState } from 'react'; + +import { DfnsApiClient, DfnsAuthenticator } from '@dfns/sdk'; +import { WebAuthnSigner } from '@dfns/sdk-browser'; +import { DFNSCustomerConfiguration } from '@models/configuration'; +import { BitcoinWalletType } from '@models/wallet'; +import { BitcoinWalletContext } from '@providers/bitcoin-wallet-context-provider'; +import { DFNSDLCHandler } from 'dlc-btc-lib'; +import { RawVault, Transaction } from 'dlc-btc-lib/models'; +import { shiftValue } from 'dlc-btc-lib/utilities'; +import { bytesToHex } from 'viem'; + +import { BITCOIN_NETWORK_MAP } from '@shared/constants/bitcoin.constants'; + +const BITCOIN_NETWORK_DFNS_MAP = { + mainnet: 'Bitcoin', + testnet: 'BitcoinTestnet3', + regtest: 'BitcoinTestnet3', +}; + +interface UseDFNSReturnType { + connectDFNSWallet: ( + userName: string, + dfnsConfiguration: DFNSCustomerConfiguration + ) => Promise<{ address: string | undefined; walletID: string }[]>; + registerCredentials: ( + code: string, + dfnsConfiguration: DFNSCustomerConfiguration + ) => Promise; + selectWallet: (walletId: string) => Promise; + handleFundingTransaction: ( + dlcHandler: DFNSDLCHandler, + vault: RawVault, + bitcoinAmount: number, + attestorGroupPublicKey: string, + feeRateMultiplier: number + ) => Promise; + handleDepositTransaction: ( + dlcHandler: DFNSDLCHandler, + vault: RawVault, + depositAmount: number, + attestorGroupPublicKey: string, + feeRateMultiplier: number + ) => Promise; + handleWithdrawalTransaction: ( + dlcHandler: DFNSDLCHandler, + withdrawAmount: number, + attestorGroupPublicKey: string, + vault: RawVault, + feeRateMultiplier: number + ) => Promise; + isLoading: [boolean, string]; +} + +export function useDFNS(): UseDFNSReturnType { + const { setDLCHandler, setBitcoinWalletType, dlcHandler } = useContext(BitcoinWalletContext); + + const [isLoading, setIsLoading] = useState<[boolean, string]>([false, '']); + + async function connectDFNSWallet( + userName: string, + dfnsConfiguration: DFNSCustomerConfiguration + ): Promise<{ address: string | undefined; walletID: string }[]> { + try { + setIsLoading([true, 'Connecting to DFNS Wallet']); + + const { dfnsBaseURL } = appConfiguration.dfnsConfiguration; + + const { applicationID, organizationID } = dfnsConfiguration; + + const signer = new WebAuthnSigner(); + const authApi = new DfnsAuthenticator({ + appId: applicationID, + baseUrl: dfnsBaseURL, + signer, + }); + + const { token } = await authApi.login({ + username: userName, + orgId: organizationID, + }); + + const dfnsDLCHandler = new DFNSDLCHandler( + applicationID, + dfnsBaseURL, + token, + BITCOIN_NETWORK_MAP[appConfiguration.bitcoinNetwork], + appConfiguration.bitcoinBlockchainURL, + appConfiguration.bitcoinBlockchainFeeEstimateURL + ); + + setDLCHandler(dfnsDLCHandler); + setBitcoinWalletType(BitcoinWalletType.DFNS); + setIsLoading([false, '']); + + return (await dfnsDLCHandler.getWallets()).items + .filter( + item => + item.network === BITCOIN_NETWORK_DFNS_MAP[appConfiguration.bitcoinNetwork] && + item.signingKey.scheme === 'Schnorr' && + item.signingKey.curve === 'secp256k1' + ) + .map(item => ({ + address: item.address, + walletID: item.id, + })); + } catch (error) { + setIsLoading([false, '']); + throw new Error(`Error connecting to DFNS Wallet: ${error}`); + } + } + + async function selectWallet(walletId: string): Promise { + try { + setIsLoading([true, 'Selecting DFNS Wallet']); + + await (dlcHandler as DFNSDLCHandler).initializeWalletByID(walletId); + + setIsLoading([false, '']); + } catch (error) { + setIsLoading([false, '']); + throw new Error(`Error selecting Wallet: ${error}`); + } + } + + async function registerCredentials( + code: string, + dfnsConfiguration: DFNSCustomerConfiguration + ): Promise { + const { dfnsBaseURL } = appConfiguration.dfnsConfiguration; + + if (!dfnsConfiguration) { + throw new Error('DFNS Configuration not found'); + } + + const { applicationID } = dfnsConfiguration; + + const signer = new WebAuthnSigner(); + const dfnsAPI = new DfnsApiClient({ + appId: applicationID, + authToken: undefined, + baseUrl: dfnsBaseURL, + signer, + }); + + const challenge = await dfnsAPI.auth.createCredentialChallengeWithCode({ + body: { code, credentialKind: 'Fido2' }, + }); + + if (challenge.kind !== 'Fido2') { + throw Error('Not a Fido2 challenge'); // this check is meant for proper typescript type inferrence + } + + const attestation = await new WebAuthnSigner().create(challenge); + + await dfnsAPI.auth.createCredentialWithCode({ + body: { + credentialName: 'iBTC App - ' + new Date().toISOString(), + challengeIdentifier: challenge.challengeIdentifier, + credentialKind: attestation.credentialKind, + credentialInfo: attestation.credentialInfo, + }, + }); + } + + /** + * Creates the Funding Transaction and signs it with Leather Wallet. + * @param vaultUUID The Vault UUID. + * @returns The Signed Funding Transaction. + */ + async function handleFundingTransaction( + dlcHandler: DFNSDLCHandler, + vault: RawVault, + bitcoinAmount: number, + attestorGroupPublicKey: string, + feeRateMultiplier: number + ): Promise { + try { + setIsLoading([true, 'Creating Funding Transaction']); + + // ==> Create Funding Transaction + const fundingPSBT = await dlcHandler?.createFundingPSBT( + vault, + BigInt(shiftValue(bitcoinAmount)), + attestorGroupPublicKey, + feeRateMultiplier + ); + + setIsLoading([true, 'Sign Funding Transaction in your DFNS Wallet']); + + const signedFundingTransaction = await dlcHandler.signPSBT(fundingPSBT, 'funding'); + + setIsLoading([false, '']); + return signedFundingTransaction; + } catch (error) { + setIsLoading([false, '']); + throw new Error(`Error handling Funding Transaction: ${error}`); + } + } + + async function handleDepositTransaction( + dlcHandler: DFNSDLCHandler, + vault: RawVault, + depositAmount: number, + attestorGroupPublicKey: string, + feeRateMultiplier: number + ): Promise { + try { + setIsLoading([true, 'Creating Deposit Transaction']); + + const depositPSBT = await dlcHandler.createDepositPSBT( + BigInt(shiftValue(depositAmount)), + vault, + attestorGroupPublicKey, + vault.fundingTxId, + feeRateMultiplier + ); + + setIsLoading([true, 'Sign Deposit Transaction in your DFNS Wallet']); + + const signedDepositTransaction = await dlcHandler.signPSBT(depositPSBT, 'deposit'); + + setIsLoading([false, '']); + return signedDepositTransaction; + } catch (error) { + setIsLoading([false, '']); + throw new Error(`Error handling Deposit Transaction: ${error}`); + } + } + + async function handleWithdrawalTransaction( + dlcHandler: DFNSDLCHandler, + withdrawAmount: number, + attestorGroupPublicKey: string, + vault: RawVault, + feeRateMultiplier: number + ): Promise { + try { + setIsLoading([true, 'Creating Withdraw Transaction']); + + const withdrawalPSBT = await dlcHandler.createWithdrawPSBT( + vault, + BigInt(shiftValue(withdrawAmount)), + attestorGroupPublicKey, + vault.fundingTxId, + feeRateMultiplier + ); + + setIsLoading([true, 'Sign Withdraw Transaction in your DFNS Wallet']); + + const signedWithdrawTransaction = await dlcHandler.signPSBT(withdrawalPSBT, 'withdraw'); + + setIsLoading([false, '']); + const psbtHex = bytesToHex(signedWithdrawTransaction.toPSBT()); + + return psbtHex.slice(2); + } catch (error) { + setIsLoading([false, '']); + throw new Error(`Error handling Withdrawal Transaction: ${error}`); + } + } + + return { + connectDFNSWallet, + registerCredentials, + selectWallet, + handleFundingTransaction, + handleDepositTransaction, + handleWithdrawalTransaction, + isLoading, + }; +} diff --git a/src/app/hooks/use-psbt.ts b/src/app/hooks/use-psbt.ts index d9217142..8f705eec 100644 --- a/src/app/hooks/use-psbt.ts +++ b/src/app/hooks/use-psbt.ts @@ -9,7 +9,7 @@ import { EthereumNetworkConfigurationContext } from '@providers/ethereum-network import { NetworkConfigurationContext } from '@providers/network-configuration.provider'; import { RippleNetworkConfigurationContext } from '@providers/ripple-network-configuration.provider'; import { XRPWalletContext } from '@providers/xrp-wallet-context-provider'; -import { LedgerDLCHandler, SoftwareWalletDLCHandler } from 'dlc-btc-lib'; +import { DFNSDLCHandler, LedgerDLCHandler, SoftwareWalletDLCHandler } from 'dlc-btc-lib'; import { submitFundingPSBT, submitWithdrawDepositPSBT, @@ -21,6 +21,7 @@ import { useAccount } from 'wagmi'; import { NetworkType } from '@shared/constants/network.constants'; +import { useDFNS } from './use-dfns'; import { useLeather } from './use-leather'; import { useLedger } from './use-ledger'; import { useUnisat } from './use-unisat'; @@ -69,6 +70,13 @@ export function usePSBT(): UsePSBTReturnType { isLoading: isUnisatLoading, } = useUnisat(); + const { + handleFundingTransaction: handleFundingTransactionWithDFNS, + handleDepositTransaction: handleDepositTransactionWithDFNS, + handleWithdrawalTransaction: handleWithdrawalTransactionWithDFNS, + isLoading: isDFNSLoading, + } = useDFNS(); + const [bitcoinDepositAmount, setBitcoinDepositAmount] = useState(0); const attestorChainIDs = { @@ -138,6 +146,27 @@ export function usePSBT(): UsePSBTReturnType { ); } break; + case 'DFNS': + switch (vault.valueLocked.toNumber()) { + case 0: + fundingTransaction = await handleFundingTransactionWithDFNS( + dlcHandler as DFNSDLCHandler, + vault, + depositAmount, + attestorGroupPublicKey, + feeRateMultiplier + ); + break; + default: + fundingTransaction = await handleDepositTransactionWithDFNS( + dlcHandler as DFNSDLCHandler, + vault, + depositAmount, + attestorGroupPublicKey, + feeRateMultiplier + ); + } + break; case 'Unisat': switch (vault.valueLocked.toNumber()) { case 0: @@ -185,14 +214,16 @@ export function usePSBT(): UsePSBTReturnType { default: throw new BitcoinError('Invalid Bitcoin Wallet Type'); } - switch (vault.status) { case VaultState.READY: await submitFundingPSBT([appConfiguration.coordinatorURL], { vaultUUID, fundingPSBT: bytesToHex(fundingTransaction.toPSBT()), userEthereumAddress: userAddress, - userBitcoinTaprootPublicKey: dlcHandler.getTaprootDerivedPublicKey(), + userBitcoinTaprootPublicKey: + bitcoinWalletType === 'DFNS' + ? `02${(dlcHandler as DFNSDLCHandler).getTaprootTweakedPublicKey()}` + : dlcHandler.getTaprootDerivedPublicKey(), attestorChainID: attestorChainIDs[networkType] as AttestorChainID, }); break; @@ -250,6 +281,15 @@ export function usePSBT(): UsePSBTReturnType { feeRateMultiplier ); break; + case 'DFNS': + withdrawalTransactionHex = await handleWithdrawalTransactionWithDFNS( + dlcHandler as DFNSDLCHandler, + withdrawAmount, + attestorGroupPublicKey, + vault, + feeRateMultiplier + ); + break; default: throw new BitcoinError('Invalid Bitcoin Wallet Type'); } @@ -270,6 +310,7 @@ export function usePSBT(): UsePSBTReturnType { [BitcoinWalletType.Leather]: isLeatherLoading, [BitcoinWalletType.Unisat]: isUnisatLoading, [BitcoinWalletType.Fordefi]: isUnisatLoading, + [BitcoinWalletType.DFNS]: isDFNSLoading, }; return { diff --git a/src/app/providers/bitcoin-wallet-context-provider.tsx b/src/app/providers/bitcoin-wallet-context-provider.tsx index 1baa1294..70a6f829 100644 --- a/src/app/providers/bitcoin-wallet-context-provider.tsx +++ b/src/app/providers/bitcoin-wallet-context-provider.tsx @@ -2,7 +2,7 @@ import { createContext, useState } from 'react'; import { HasChildren } from '@models/has-children'; import { BitcoinWalletType } from '@models/wallet'; -import { LedgerDLCHandler, SoftwareWalletDLCHandler } from 'dlc-btc-lib'; +import { DFNSDLCHandler, LedgerDLCHandler, SoftwareWalletDLCHandler } from 'dlc-btc-lib'; export enum BitcoinWalletContextState { INITIAL = 0, @@ -15,9 +15,9 @@ interface BitcoinWalletContextProviderType { setBitcoinWalletType: React.Dispatch>; bitcoinWalletContextState: BitcoinWalletContextState; setBitcoinWalletContextState: React.Dispatch>; - dlcHandler: SoftwareWalletDLCHandler | LedgerDLCHandler | undefined; + dlcHandler: SoftwareWalletDLCHandler | LedgerDLCHandler | DFNSDLCHandler | undefined; setDLCHandler: React.Dispatch< - React.SetStateAction + React.SetStateAction >; resetBitcoinWalletContext: () => void; } @@ -38,7 +38,9 @@ export function BitcoinWalletContextProvider({ children }: HasChildren): React.J const [bitcoinWalletType, setBitcoinWalletType] = useState( BitcoinWalletType.Leather ); - const [dlcHandler, setDLCHandler] = useState(); + const [dlcHandler, setDLCHandler] = useState< + SoftwareWalletDLCHandler | LedgerDLCHandler | DFNSDLCHandler + >(); function resetBitcoinWalletContext() { setBitcoinWalletContextState(BitcoinWalletContextState.INITIAL); diff --git a/src/app/store/slices/modal/modal.slice.ts b/src/app/store/slices/modal/modal.slice.ts index ef32b01b..f6a15234 100644 --- a/src/app/store/slices/modal/modal.slice.ts +++ b/src/app/store/slices/modal/modal.slice.ts @@ -6,6 +6,7 @@ interface ModalState { isSuccesfulFlowModalOpen: [boolean, Vault | undefined, string, string, number]; isSelectBitcoinWalletModalOpen: boolean; isLedgerModalOpen: boolean; + isDFNSModalOpen: boolean; } const initialModalState: ModalState = { @@ -13,6 +14,7 @@ const initialModalState: ModalState = { isSuccesfulFlowModalOpen: [false, undefined, '', 'mint', 0], isSelectBitcoinWalletModalOpen: false, isLedgerModalOpen: false, + isDFNSModalOpen: true, }; export const modalSlice = createSlice({ @@ -38,5 +40,8 @@ export const modalSlice = createSlice({ toggleLedgerModalVisibility: state => { state.isLedgerModalOpen = !state.isLedgerModalOpen; }, + toggleDFNSModalVisibility: state => { + state.isDFNSModalOpen = !state.isDFNSModalOpen; + }, }, }); diff --git a/src/shared/models/configuration.ts b/src/shared/models/configuration.ts index 14ef6743..fe77457b 100644 --- a/src/shared/models/configuration.ts +++ b/src/shared/models/configuration.ts @@ -17,6 +17,17 @@ enum AppEnvironment { LOCALHOST = 'localhost', } +export interface DFNSCustomerConfiguration { + name: string; + organizationID: string; + applicationID: string; +} + +interface DFNSConfiguration { + dfnsBaseURL: string; + dfnsCustomerConfigurations: DFNSCustomerConfiguration[]; +} + type BitcoinNetworkPrefix = 'bc1' | 'tb1' | 'bcrt1'; export const ALL_SUPPORTED_BITCOIN_NETWORK_PREFIX: BitcoinNetworkPrefix[] = ['bc1', 'tb1', 'bcrt1']; @@ -42,6 +53,7 @@ export interface Configuration { bitcoinBlockchainFeeEstimateURL: string; rippleIssuerAddress: string; ledgerApp: string; + dfnsConfiguration: DFNSConfiguration; merchants: Merchant[]; protocols: Protocol[]; } diff --git a/src/shared/models/wallet.ts b/src/shared/models/wallet.ts index 596427a5..49e649d1 100644 --- a/src/shared/models/wallet.ts +++ b/src/shared/models/wallet.ts @@ -3,6 +3,7 @@ export enum BitcoinWalletType { Ledger = 'Ledger', Unisat = 'Unisat', Fordefi = 'Fordefi', + DFNS = 'DFNS', } export interface BitcoinWallet { @@ -35,6 +36,12 @@ const fordefi: BitcoinWallet = { logo: '/images/logos/fordefi-logo.svg', }; +const dfns: BitcoinWallet = { + id: BitcoinWalletType.DFNS, + name: 'DFNS', + logo: '/images/logos/dfns-logo.svg', +}; + export enum XRPWalletType { Ledger = 'Ledger', Gem = 'Gem', @@ -59,4 +66,4 @@ const gemXRP: XRPWallet = { }; export const xrpWallets: XRPWallet[] = [ledgerXRP, gemXRP]; -export const bitcoinWallets: BitcoinWallet[] = [leather, ledger, unisat, fordefi]; +export const bitcoinWallets: BitcoinWallet[] = [leather, ledger, unisat, fordefi, dfns]; diff --git a/src/styles/button-theme.ts b/src/styles/button-theme.ts index 56c887a7..addf6fce 100644 --- a/src/styles/button-theme.ts +++ b/src/styles/button-theme.ts @@ -111,6 +111,18 @@ const ledger = defineStyle({ }, }); +const dfns = defineStyle({ + p: '10px', + h: '50px', + w: '375px', + bg: 'transparent', + border: '1.5px solid', + borderColor: '#D6D7EB', + _hover: { + bgColor: 'white.03', + }, +}); + const bitcoinAddress = defineStyle({ bg: 'transparent', px: '2.5%', @@ -170,6 +182,7 @@ export const buttonTheme = defineStyleConfig({ navigate, merchantHistory, ledger, + dfns, points, merchantTableItem, }, diff --git a/src/styles/css-styles.ts b/src/styles/css-styles.ts index 8a71de02..873e0d7f 100644 --- a/src/styles/css-styles.ts +++ b/src/styles/css-styles.ts @@ -13,6 +13,19 @@ export const scrollBarCSS = { }, }; +export const scrollBarDFNSAddressCSS = { + '&::-webkit-scrollbar': { + background: 'rgba(255,255,255,0.25)', + width: '3.5px', + }, + '&::-webkit-scrollbar-track': { + width: '2.5px', + }, + '&::-webkit-scrollbar-thumb': { + background: '#E1FF0B', + }, +}; + export const boxShadowAnimation = keyframes` 0% { box-shadow: 0 0 5px rgba(255,255,255,0); } 25% { box-shadow: 0 0 10px rgba(255,255,255,0.5); } diff --git a/src/styles/menu-theme.ts b/src/styles/menu-theme.ts index f76b0c82..ea2cf2cd 100644 --- a/src/styles/menu-theme.ts +++ b/src/styles/menu-theme.ts @@ -125,6 +125,45 @@ const network = definePartsStyle({ }, }); +const organization = definePartsStyle({ + button: { + justifyContent: 'center', + p: '10px', + h: '50px', + w: '375px', + bg: 'transparent', + border: '1px solid', + borderColor: 'white.01', + borderRadius: 'md', + color: 'white', + fontSize: 'sm', + fontWeight: 600, + _hover: { + background: 'white.03', + }, + }, + list: { + p: '10px', + w: '375px', + bgColor: '#170C33', + border: '1.5px solid', + borderColor: 'border.white.01', + borderRadius: 'md', + }, + item: { + justifyContent: 'center', + bgColor: 'inherit', + borderRadius: 'md', + color: 'white', + fontSize: 'xs', + fontWeight: 400, + _hover: { + background: 'white.03', + }, + transition: 'all 0.05s ease-in-out', + }, +}); + const ledgerAddress = definePartsStyle({ button: { justifyContent: 'center', @@ -184,6 +223,7 @@ const variants = { account, ledgerAddress, networkChange, + organization, }; export const menuTheme = defineMultiStyleConfig({ sizes, variants }); diff --git a/src/styles/modal-theme.ts b/src/styles/modal-theme.ts index 97e04200..4ec9205c 100644 --- a/src/styles/modal-theme.ts +++ b/src/styles/modal-theme.ts @@ -37,8 +37,27 @@ const ledgerModalStyle = definePartsStyle({ }, }); +const dfnsModalStyle = definePartsStyle({ + dialogContainer: { + top: '13.5%', + }, + dialog: { + padding: '15px', + fontFamily: 'Inter', + height: 'auto', + width: '500px', + border: '1.5px solid', + borderColor: '#E1FF0B', + borderRadius: 'md', + backgroundColor: '#170C33', + color: '#D6D7EB', + alignItems: 'center', + }, +}); + const variants = { ledger: ledgerModalStyle, + dfns: dfnsModalStyle, }; export const modalTheme = defineMultiStyleConfig({ diff --git a/yarn.lock b/yarn.lock index 8395a43a..e2dbc0f7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1117,6 +1117,24 @@ preact "^10.16.0" sha.js "^2.4.11" +"@dfns/sdk-browser@^0.5.9": + version "0.5.9" + resolved "https://registry.yarnpkg.com/@dfns/sdk-browser/-/sdk-browser-0.5.9.tgz#073bdc2fabb6b53eafd114a31ebf355be9d66062" + integrity sha512-lC0RnE61PC4wrDRAizyLk5gvgiPiK7dkLfETfJ+X8lO1B95+ovRcR9Md2so2z3EpCdBgGCQ4IKKk6ybYbp78Ag== + dependencies: + buffer "6.0.3" + cross-fetch "3.1.6" + uuid "9.0.0" + +"@dfns/sdk@^0.5.9": + version "0.5.9" + resolved "https://registry.yarnpkg.com/@dfns/sdk/-/sdk-0.5.9.tgz#37994f90f3397d58e7e57982043e594d06b5d851" + integrity sha512-c4DDFMvVsx5EwPc9xLK8+0MitpTPFMmyvvuZJTAYSRlG9IdXNHw8eRDHA4qjltgbraYNXWB2YtxKod6BS8Witg== + dependencies: + buffer "6.0.3" + cross-fetch "3.1.6" + uuid "9.0.0" + "@emotion/babel-plugin@^11.11.0": version "11.11.0" resolved "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz" @@ -4342,7 +4360,7 @@ buffer-from@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== -buffer@^6.0.3: +buffer@6.0.3, buffer@^6.0.3: version "6.0.3" resolved "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz" integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== @@ -4652,6 +4670,13 @@ create-hmac@^1.1.3, create-hmac@^1.1.7: safe-buffer "^5.0.1" sha.js "^2.4.8" +cross-fetch@3.1.6: + version "3.1.6" + resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.6.tgz#bae05aa31a4da760969756318feeee6e70f15d6c" + integrity sha512-riRvo06crlE8HiqOwIpQhxwdOk4fOeR7FVM/wXoxchFEqMNUjvbs3bfo4OTgMEMHzppd4DxFBDbyySj8Cv781g== + dependencies: + node-fetch "^2.6.11" + cross-fetch@^3.1.4: version "3.1.8" resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.8.tgz#0327eba65fd68a7d119f8fb2bf9334a1a7956f82" @@ -5195,11 +5220,13 @@ dir-glob@^3.0.1: dependencies: path-type "^4.0.0" -dlc-btc-lib@2.4.18: - version "2.4.18" - resolved "https://registry.yarnpkg.com/dlc-btc-lib/-/dlc-btc-lib-2.4.18.tgz#246c15af91bfcf03212f8abd440b122f575510f4" - integrity sha512-Q7t8VGrrbA2ioyNvvNXxH8dfqQwFUDLpLNWHwqqY0H8zaD4gXpKswi1clDHy7gASZhcLvBBOKAYp5+PUkja/YA== +dlc-btc-lib@2.4.19-dfns-beta: + version "2.4.19-dfns-beta" + resolved "https://registry.yarnpkg.com/dlc-btc-lib/-/dlc-btc-lib-2.4.19-dfns-beta.tgz#4d9ddb7591a0d3f5cc99d09a7e25cea7aece77e3" + integrity sha512-niqR5KixLyrxgR91DzcDfpb5sW7IEWtHqpsLRTFFI679hcPabglYI5l5NsNc6UZ08DHI7ecd2/ZKnEXlMMxCpg== dependencies: + "@dfns/sdk" "^0.5.9" + "@dfns/sdk-browser" "^0.5.9" "@gemwallet/api" "3.8.0" "@ledgerhq/hw-app-btc" "10.4.1" "@ledgerhq/hw-app-xrp" "6.29.4" @@ -7164,7 +7191,7 @@ node-fetch-native@^1.6.2, node-fetch-native@^1.6.3, node-fetch-native@^1.6.4: resolved "https://registry.yarnpkg.com/node-fetch-native/-/node-fetch-native-1.6.4.tgz#679fc8fd8111266d47d7e72c379f1bed9acff06e" integrity sha512-IhOigYzAKHd244OC0JIMIUrjzctirCmPkaIfhDeGcEETWof5zKYUW7e7MYvChGWh/4CJeXEgsRyGzuF334rOOQ== -node-fetch@^2.6.1, node-fetch@^2.6.12: +node-fetch@^2.6.1, node-fetch@^2.6.11, node-fetch@^2.6.12: version "2.7.0" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== @@ -8964,6 +8991,11 @@ util@^0.12.3, util@^0.12.4: is-typed-array "^1.1.3" which-typed-array "^1.1.2" +uuid@9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.0.tgz#592f550650024a38ceb0c562f2f6aa435761efb5" + integrity sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg== + uuid@^8.3.2: version "8.3.2" resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"