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/package.json b/package.json
index 2d934487..c00b6a6d 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.21",
"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..04467c60 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';
@@ -32,6 +33,16 @@ interface UsePSBTReturnType {
isLoading: [boolean, string];
}
+const getRequiredTaprootPublicKey = (
+ bitcoinWalletType: BitcoinWalletType,
+ dlcHandler: LedgerDLCHandler | SoftwareWalletDLCHandler | DFNSDLCHandler
+): string => {
+ if (bitcoinWalletType === BitcoinWalletType.DFNS && dlcHandler instanceof DFNSDLCHandler) {
+ return `02${dlcHandler.getTaprootTweakedPublicKey()}`;
+ }
+ return dlcHandler.getTaprootDerivedPublicKey();
+};
+
export function usePSBT(): UsePSBTReturnType {
const {
ethereumNetworkConfiguration: { dlcManagerContract, ethereumAttestorChainID },
@@ -69,6 +80,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 +156,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 +224,13 @@ 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: getRequiredTaprootPublicKey(bitcoinWalletType, dlcHandler),
attestorChainID: attestorChainIDs[networkType] as AttestorChainID,
});
break;
@@ -250,6 +288,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 +317,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..03e94454 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: false,
};
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/updates.patch b/updates.patch
new file mode 100644
index 00000000..95d61573
--- /dev/null
+++ b/updates.patch
@@ -0,0 +1,1484 @@
+diff --git a/config.devnet.json b/config.devnet.json
+index bf03854..5e908bf 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 ecb7218..8e10ac6 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 5e039a5..fdd81e6 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/package.json b/package.json
+index 2d93448..4251a8b 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 0000000..0cbb18d
+--- /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 84b71b2..53acc5b 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 0000000..20dbe82
+--- /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 0000000..24ba22a
+--- /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 0000000..80713c9
+--- /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 0000000..380cc2b
+--- /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 0000000..95dc93b
+--- /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 0000000..d7fb2d3
+--- /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 0000000..1bf61e6
+--- /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 0000000..4b461a9
+--- /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 0000000..1f7b73b
+--- /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 0000000..03b65be
+--- /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 0000000..6e02cad
+--- /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 a8c78ea..2047c65 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 6e7899d..64c7d8b 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 0000000..9b3c92b
+--- /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 d921714..8f705ee 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 1baa129..70a6f82 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 ef32b01..f6a1523 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 14ef674..fe77457 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 596427a..49e649d 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 56c887a..addf6fc 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 8a71de0..873e0d7 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 f76b0c8..ea2cf2c 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 97e0420..4ec9205 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 8395a43..e2dbc0f 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"
diff --git a/yarn.lock b/yarn.lock
index 8395a43a..ebeb17c4 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.21:
+ version "2.4.21"
+ resolved "https://registry.yarnpkg.com/dlc-btc-lib/-/dlc-btc-lib-2.4.21.tgz#5e7294c413bb4f8a66fe2ba0ef05ccd011faec24"
+ integrity sha512-56EyZ9Udd98pPWTKRj/PGIie9wxt1lupmOqqQD4CL1YKxobtIdmD8Uv9/d0uczpEXyQRyqnxAycTIBPvknPJxg==
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"