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 (
+
+
+
+ );
+}
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 (
+
+
+
+ );
+}
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 (
+
+
+
+
+
+
+
+
+
+ {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
+
+
+
+ );
+}
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"