From fd97e619301f74615af81c014576b71e19c8fdc4 Mon Sep 17 00:00:00 2001 From: Polybius93 <99192647+Polybius93@users.noreply.github.com> Date: Thu, 21 Nov 2024 10:06:50 +0100 Subject: [PATCH] feat: add xrpl (#202) * feat: add xrpl --- config.devnet.json | 3 + config.mainnet.json | 3 + config.testnet.json | 3 + .../fetch-attestor-group-public-key.ts | 42 +++ .../functions/submit-xrpl-vault-request.ts | 62 ++++ package.json | 7 +- public/images/logos/gem-wallet-logo.svg | 1 + public/images/logos/xrp-logo.svg | 16 + src/app/app.tsx | 65 ++-- src/app/components/account/account.tsx | 52 +++- .../account/components/account-menu.tsx | 13 +- src/app/components/header/header.tsx | 16 +- .../bottom/components/how-to-mint.tsx | 3 +- .../bottom/components/how-to-unmint.tsx | 5 +- .../burn-transaction-screen.tsx | 66 ++-- .../deposit-transaction-screen.tsx | 19 +- .../mint-unmint/components/mint/mint.tsx | 11 +- .../setup-vault-screen/setup-vault-screen.tsx | 28 +- .../components/unmint-vault-selector.tsx | 12 +- .../unmint/components/withdraw-screen.tsx | 10 +- .../mint-unmint/components/unmint/unmint.tsx | 11 +- .../components/walkthrough-blockchain-tag.tsx | 10 +- .../components/walkthrough-header.tsx | 4 +- .../components/walkthrough/walkthrough.tsx | 129 ++++---- .../modals/components/modal-container.tsx | 8 +- ...nu.tsx => select-ethereum-wallet-menu.tsx} | 6 +- .../components/select-ripple-wallet-menu.tsx | 33 ++ .../select-wallet-modal.tsx | 179 +++++++++-- .../successful-flow-modal.tsx | 15 +- .../my-vaults-small/my-vaults-small.tsx | 51 ++-- .../my-vaults-header/my-vaults-header.tsx | 8 +- .../components/my-vaults/my-vaults-large.tsx | 47 +-- .../network/components/networks-menu.tsx | 18 +- .../protocol-history-table-item.tsx | 1 - .../select-network-button.tsx | 57 +++- ...on-screen.transaction-form.field-input.tsx | 2 +- ...transaction-form.navigate-button-group.tsx | 24 +- ...n.transaction-form.submit-button-group.tsx | 10 +- .../vault.detaills/vault.details.tsx | 24 +- src/app/components/vault/vault.tsx | 6 +- .../vaults-list-group-container.tsx | 11 +- .../functions/attestor-request.functions.ts | 36 +++ src/app/functions/configuration.functions.ts | 10 + src/app/functions/vault.functions.ts | 9 - src/app/hooks/use-active-tabs.ts | 33 +- src/app/hooks/use-connected.ts | 35 +++ src/app/hooks/use-deposit-limits.ts | 35 ++- src/app/hooks/use-ethereum-observer.ts | 149 --------- src/app/hooks/use-evm-vaults.ts | 204 +++++++++++++ src/app/hooks/use-psbt.ts | 73 +++-- src/app/hooks/use-vaults.ts | 126 -------- src/app/hooks/use-xrp-wallet.ts | 89 ++++++ src/app/hooks/use-xrpl-gem.ts | 109 +++++++ src/app/hooks/use-xrpl-ledger.ts | 165 ++++++++++ src/app/hooks/use-xrpl-vaults.ts | 200 ++++++++++++ src/app/pages/dashboard/dashboard.tsx | 6 +- .../providers/balance-context-provider.tsx | 64 ++-- .../providers/ethereum-observer-provider.tsx | 8 - .../network-configuration.provider.tsx | 29 ++ .../providers/network-connection.provider.tsx | 25 ++ .../ripple-network-configuration.provider.tsx | 106 +++++++ src/app/providers/vault-context-provider.tsx | 39 ++- .../providers/xrp-wallet-context-provider.tsx | 71 +++++ src/app/store/index.ts | 3 - .../slices/mintunmint/mintunmint.slice.ts | 44 ++- src/app/store/slices/modal/modal.slice.ts | 8 +- src/app/store/slices/vault/vault.actions.ts | 3 - src/app/store/slices/vault/vault.slice.ts | 55 ---- src/shared/constants/network.constants.ts | 5 + src/shared/constants/ripple.constants.ts | 14 + src/shared/models/configuration.ts | 4 + src/shared/models/error-types.ts | 7 + src/shared/models/ledger.ts | 1 + src/shared/models/ripple.models.ts | 17 ++ src/shared/models/wallet.ts | 24 ++ yarn.lock | 286 +++++++++++++----- 76 files changed, 2357 insertions(+), 826 deletions(-) create mode 100644 netlify/functions/fetch-attestor-group-public-key.ts create mode 100644 netlify/functions/submit-xrpl-vault-request.ts create mode 100644 public/images/logos/gem-wallet-logo.svg create mode 100644 public/images/logos/xrp-logo.svg rename src/app/components/modals/select-wallet-modal/components/{select-wallet-menu.tsx => select-ethereum-wallet-menu.tsx} (91%) create mode 100644 src/app/components/modals/select-wallet-modal/components/select-ripple-wallet-menu.tsx create mode 100644 src/app/functions/attestor-request.functions.ts create mode 100644 src/app/hooks/use-connected.ts delete mode 100644 src/app/hooks/use-ethereum-observer.ts create mode 100644 src/app/hooks/use-evm-vaults.ts delete mode 100644 src/app/hooks/use-vaults.ts create mode 100644 src/app/hooks/use-xrp-wallet.ts create mode 100644 src/app/hooks/use-xrpl-gem.ts create mode 100644 src/app/hooks/use-xrpl-ledger.ts create mode 100644 src/app/hooks/use-xrpl-vaults.ts delete mode 100644 src/app/providers/ethereum-observer-provider.tsx create mode 100644 src/app/providers/network-configuration.provider.tsx create mode 100644 src/app/providers/network-connection.provider.tsx create mode 100644 src/app/providers/ripple-network-configuration.provider.tsx create mode 100644 src/app/providers/xrp-wallet-context-provider.tsx delete mode 100644 src/app/store/slices/vault/vault.actions.ts delete mode 100644 src/app/store/slices/vault/vault.slice.ts create mode 100644 src/shared/constants/network.constants.ts create mode 100644 src/shared/constants/ripple.constants.ts create mode 100644 src/shared/models/ripple.models.ts diff --git a/config.devnet.json b/config.devnet.json index 524e8cd6..da279717 100644 --- a/config.devnet.json +++ b/config.devnet.json @@ -2,12 +2,15 @@ "appEnvironment": "devnet", "coordinatorURL": "https://devnet.dlc.link/attestor-1", "enabledEthereumNetworkIDs": ["421614", "84532", "11155111"], + "enabledRippleNetworkIDs": ["1"], "bitcoinNetwork": "regtest", "bitcoinNetworkIndex": 1, "bitcoinNetworkPreFix": "bcrt1", "bitcoinBlockchainURL": "https://devnet.dlc.link/electrs", "bitcoinBlockchainExplorerURL": "https://devnet.dlc.link/electrs", "bitcoinBlockchainFeeEstimateURL": "https://devnet.dlc.link/electrs/fee-estimates", + "rippleIssuerAddress": "rLTBw1MEy45uE5qmkWseinbj7h4zmdQuR8", + "xrplWebsocket": "wss://s.altnet.rippletest.net:51233", "ledgerApp": "Bitcoin Test", "merchants": [ { diff --git a/config.mainnet.json b/config.mainnet.json index a545d9ed..30288755 100644 --- a/config.mainnet.json +++ b/config.mainnet.json @@ -2,12 +2,15 @@ "appEnvironment": "mainnet", "coordinatorURL": "https://mainnet.dlc.link/attestor-1", "enabledEthereumNetworkIDs": ["42161", "8453"], + "enabledRippleNetworkIDs": ["0"], "bitcoinNetwork": "mainnet", "bitcoinNetworkIndex": 0, "bitcoinNetworkPreFix": "bc1", "bitcoinBlockchainURL": "https://mainnet.dlc.link/electrs", "bitcoinBlockchainExplorerURL": "https://mempool.space", "bitcoinBlockchainFeeEstimateURL": "https://mempool.space/api/v1/fees/recommended", + "rippleIssuerAddress": "rGcyRGrZPaJAZbZDi4NqRFLA5GQH63iFpD", + "xrplWebsocket": "wss://xrpl.ws/", "ledgerApp": "Bitcoin", "merchants": [ { diff --git a/config.testnet.json b/config.testnet.json index 5b62a442..ac5dad86 100644 --- a/config.testnet.json +++ b/config.testnet.json @@ -2,12 +2,15 @@ "appEnvironment": "testnet", "coordinatorURL": "https://testnet.dlc.link/attestor-1", "enabledEthereumNetworkIDs": ["421614", "84532", "11155111"], + "enabledRippleNetworkIDs": ["1"], "bitcoinNetwork": "testnet", "bitcoinNetworkIndex": 1, "bitcoinNetworkPreFix": "tb1", "bitcoinBlockchainURL": "https://testnet.dlc.link/electrs", "bitcoinBlockchainExplorerURL": "https://mempool.space/testnet", "bitcoinBlockchainFeeEstimateURL": "https://mempool.space/testnet/api/v1/fees/recommended", + "rippleIssuerAddress": "ra3oyRVfy4yD4NJPrVcewvDtisZ3FhkcYL", + "xrplWebsocket": "wss://testnet.xrpl-labs.com/", "ledgerApp": "Bitcoin Test", "merchants": [ { diff --git a/netlify/functions/fetch-attestor-group-public-key.ts b/netlify/functions/fetch-attestor-group-public-key.ts new file mode 100644 index 00000000..ad9b605f --- /dev/null +++ b/netlify/functions/fetch-attestor-group-public-key.ts @@ -0,0 +1,42 @@ +import { Handler, HandlerEvent } from '@netlify/functions'; +import { getAttestorExtendedGroupPublicKey } from 'dlc-btc-lib/attestor-request-functions'; + +const handler: Handler = async (event: HandlerEvent) => { + try { + if (!event.queryStringParameters) { + return { + statusCode: 400, + body: JSON.stringify({ + message: 'No Parameters were provided', + }), + }; + } + + if (!event.queryStringParameters.coordinatorURL) { + return { + statusCode: 400, + body: JSON.stringify({ + message: 'No Coordinator URL was provided', + }), + }; + } + + const coordinatorURL = event.queryStringParameters.coordinatorURL; + + const attestorGroupPublicKey = await getAttestorExtendedGroupPublicKey(coordinatorURL); + + return { + statusCode: 200, + body: attestorGroupPublicKey, + }; + } catch (error: any) { + return { + statusCode: 500, + body: JSON.stringify({ + message: `Failed to get Attestor Group Public Key: ${error.message}`, + }), + }; + } +}; + +export { handler }; diff --git a/netlify/functions/submit-xrpl-vault-request.ts b/netlify/functions/submit-xrpl-vault-request.ts new file mode 100644 index 00000000..2dd564db --- /dev/null +++ b/netlify/functions/submit-xrpl-vault-request.ts @@ -0,0 +1,62 @@ +import { Handler, HandlerEvent } from '@netlify/functions'; +import { submitSetupXRPLVaultRequest } from 'dlc-btc-lib/attestor-request-functions'; +import { AttestorChainID } from 'dlc-btc-lib/models'; + +const handler: Handler = async (event: HandlerEvent) => { + try { + if (!event.queryStringParameters) { + return { + statusCode: 400, + body: JSON.stringify({ + message: 'No Parameters were provided', + }), + }; + } + + if (!event.queryStringParameters.coordinatorURL) { + return { + statusCode: 400, + body: JSON.stringify({ + message: 'No Coordinator URL was provided', + }), + }; + } + + if (!event.queryStringParameters.userXRPLAddress) { + return { + statusCode: 400, + body: JSON.stringify({ + message: 'No XRPL User Address was provided', + }), + }; + } + + if (!event.queryStringParameters.attestorChainID) { + return { + statusCode: 400, + body: JSON.stringify({ + message: 'No Attestor Chain ID was provided', + }), + }; + } + + const coordinatorURL = event.queryStringParameters.coordinatorURL; + const userXRPLAddress = event.queryStringParameters.userXRPLAddress; + const attestorChainID = event.queryStringParameters.attestorChainID as AttestorChainID; + + await submitSetupXRPLVaultRequest(coordinatorURL, userXRPLAddress, attestorChainID); + + return { + statusCode: 200, + }; + } catch (error: any) { + return { + statusCode: 500, + body: JSON.stringify({ + message: `Failed to submit Setup XRPL Vault Request: ${error.message}`, + }), + }; + } +}; + +export { handler }; diff --git a/package.json b/package.json index 63b19aed..46d91a62 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,8 @@ "@fontsource/inter": "^5.0.18", "@fontsource/onest": "^5.0.3", "@fontsource/poppins": "^5.0.8", + "@gemwallet/api": "^3.8.0", + "@ledgerhq/hw-app-xrp": "^6.29.4", "@ledgerhq/hw-transport-webusb": "^6.28.6", "@netlify/functions": "^2.8.1", "@reduxjs/toolkit": "^1.9.7", @@ -44,7 +46,7 @@ "concurrently": "^8.2.2", "d3": "^7.9.0", "decimal.js": "^10.4.3", - "dlc-btc-lib": "2.2.7", + "dlc-btc-lib": "2.4.12", "dotenv": "^16.3.1", "ethers": "5.7.2", "formik": "^2.4.5", @@ -64,7 +66,8 @@ "redux-persist-expire": "^1.1.1", "viem": "2.x", "vite-plugin-toml": "^0.7.0", - "wagmi": "^2.12.2" + "wagmi": "^2.12.2", + "xrpl": "^4.0.0" }, "devDependencies": { "@ls-lint/ls-lint": "^2.2.2", diff --git a/public/images/logos/gem-wallet-logo.svg b/public/images/logos/gem-wallet-logo.svg new file mode 100644 index 00000000..592bf617 --- /dev/null +++ b/public/images/logos/gem-wallet-logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/images/logos/xrp-logo.svg b/public/images/logos/xrp-logo.svg new file mode 100644 index 00000000..b340e958 --- /dev/null +++ b/public/images/logos/xrp-logo.svg @@ -0,0 +1,16 @@ + + + + + x + + + + + + + diff --git a/src/app/app.tsx b/src/app/app.tsx index d2f0d892..f3cc5404 100644 --- a/src/app/app.tsx +++ b/src/app/app.tsx @@ -12,8 +12,11 @@ import { BalanceContextProvider } from '@providers/balance-context-provider'; import { BitcoinTransactionConfirmationsProvider } from '@providers/bitcoin-query-provider'; import { BitcoinWalletContextProvider } from '@providers/bitcoin-wallet-context-provider'; import { EthereumNetworkConfigurationContextProvider } from '@providers/ethereum-network-configuration.provider'; -import { EthereumObserverProvider } from '@providers/ethereum-observer-provider'; +import { NetworkConfigurationContextProvider } from '@providers/network-configuration.provider'; +import { NetworkConnectionContextProvider } from '@providers/network-connection.provider'; import { ProofOfReserveContextProvider } from '@providers/proof-of-reserve-context-provider'; +import { RippleNetworkConfigurationContextProvider } from '@providers/ripple-network-configuration.provider'; +import { XRPWalletContextProvider } from '@providers/xrp-wallet-context-provider'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { WagmiProvider } from 'wagmi'; @@ -28,33 +31,39 @@ export function App(): React.JSX.Element { return ( - - - - - - - - - } /> - } /> - {/* } /> */} - } /> - } /> - } - /> - } /> - } /> - - - - - - - - + + + + + + + + + + + + } /> + } /> + {/* } /> */} + } /> + } /> + } + /> + } /> + } /> + + + + + + + + + + + ); diff --git a/src/app/components/account/account.tsx b/src/app/components/account/account.tsx index f79719fc..9aa75da3 100644 --- a/src/app/components/account/account.tsx +++ b/src/app/components/account/account.tsx @@ -1,24 +1,64 @@ +import { useContext } from 'react'; import { useDispatch } from 'react-redux'; import { Button, HStack, useBreakpointValue } from '@chakra-ui/react'; import { AccountMenu } from '@components/account/components/account-menu'; +import { XRPWallet, xrpWallets } from '@models/wallet'; +import { NetworkConfigurationContext } from '@providers/network-configuration.provider'; +import { NetworkConnectionContext } from '@providers/network-connection.provider'; +import { XRPWalletContext } from '@providers/xrp-wallet-context-provider'; import { mintUnmintActions } from '@store/slices/mintunmint/mintunmint.actions'; import { modalActions } from '@store/slices/modal/modal.actions'; -import { useAccount, useDisconnect } from 'wagmi'; +import { Connector, useAccount, useDisconnect } from 'wagmi'; + +import { NetworkType } from '@shared/constants/network.constants'; export function Account(): React.JSX.Element { const dispatch = useDispatch(); - const { address, connector, isConnected } = useAccount(); - const { disconnect } = useDisconnect(); + const { isConnected } = useContext(NetworkConnectionContext); + const { networkType } = useContext(NetworkConfigurationContext); + const isMobile = useBreakpointValue({ base: true, md: false }); + const { address: ethereumUserAddress, connector: ethereumWallet } = useAccount(); + const { disconnect: disconnectEthereumWallet } = useDisconnect(); + const { + userAddress: rippleUserAddress, + xrpWalletType, + resetXRPWalletContext, + } = useContext(XRPWalletContext); + + function getWalletInformation(): { address: string; wallet: XRPWallet | Connector } | undefined { + switch (networkType) { + case NetworkType.EVM: + if (!ethereumUserAddress || !ethereumWallet) return undefined; + return { address: ethereumUserAddress, wallet: ethereumWallet }; + case NetworkType.XRPL: + if (!rippleUserAddress) return undefined; + return { + address: rippleUserAddress, + wallet: xrpWallets.find(xrpWallet => xrpWallet.id === xrpWalletType)!, + }; + default: + throw new Error('Invalid Network Type'); + } + } function onConnectWalletClick(): void { dispatch(modalActions.toggleSelectWalletModalVisibility()); } function onDisconnectWalletClick(): void { - disconnect(); + switch (networkType) { + case NetworkType.EVM: + disconnectEthereumWallet(); + break; + case NetworkType.XRPL: + resetXRPWalletContext(); + break; + default: + break; + } dispatch(mintUnmintActions.resetMintUnmintState()); } @@ -26,8 +66,8 @@ export function Account(): React.JSX.Element { {isConnected ? ( onDisconnectWalletClick()} /> ) : ( diff --git a/src/app/components/account/components/account-menu.tsx b/src/app/components/account/components/account-menu.tsx index 175ee4a8..553f7cdb 100644 --- a/src/app/components/account/components/account-menu.tsx +++ b/src/app/components/account/components/account-menu.tsx @@ -9,23 +9,24 @@ import { Text, useBreakpointValue, } from '@chakra-ui/react'; +import { XRPWallet } from '@models/wallet'; import { truncateAddress } from 'dlc-btc-lib/utilities'; import { Connector } from 'wagmi'; interface AccountMenuProps { address?: string; - wagmiConnector?: Connector; + wallet?: Connector | XRPWallet; handleDisconnectWallet: () => void; } export function AccountMenu({ address, - wagmiConnector, + wallet, handleDisconnectWallet, }: AccountMenuProps): React.JSX.Element | false { const isMobile = useBreakpointValue({ base: true, md: false }); - if (!address || !wagmiConnector) return false; + if (!address) return false; return ( @@ -35,11 +36,11 @@ export function AccountMenu({ {wagmiConnector.name} {truncateAddress(address)} diff --git a/src/app/components/header/header.tsx b/src/app/components/header/header.tsx index 50c29ed6..55e9127e 100644 --- a/src/app/components/header/header.tsx +++ b/src/app/components/header/header.tsx @@ -1,16 +1,23 @@ -import React, { useEffect, useState } from 'react'; +import React, { useContext, useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { VStack, useBreakpointValue } from '@chakra-ui/react'; +import { NetworkConfigurationContext } from '@providers/network-configuration.provider'; +import { NetworkConnectionContext } from '@providers/network-connection.provider'; import { useAccount } from 'wagmi'; +import { NetworkType } from '@shared/constants/network.constants'; + import { Banner } from './components/banner'; import DesktopHeader from './components/desktop-header'; import MobileHeader from './components/mobile-header'; export function Header(): React.JSX.Element { const navigate = useNavigate(); - const { chain, isConnected } = useAccount(); + const { isConnected } = useContext(NetworkConnectionContext); + + const { networkType } = useContext(NetworkConfigurationContext); + const { chain: ethereumNetwork } = useAccount(); const [showBanner, setShowBanner] = useState(false); const [isNetworkMenuOpen, setIsNetworkMenuOpen] = useState(false); @@ -22,14 +29,13 @@ export function Header(): React.JSX.Element { const isMobile = useBreakpointValue({ base: true, md: false }); useEffect(() => { - if (isConnected && !chain) { + if (networkType === NetworkType.EVM && isConnected && !ethereumNetwork) { setShowBanner(true); } else { setShowBanner(false); } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [chain]); + }, [isConnected, ethereumNetwork]); return ( diff --git a/src/app/components/how-it-works/bottom/components/how-to-mint.tsx b/src/app/components/how-it-works/bottom/components/how-to-mint.tsx index 3d1cc3a8..b5b56e0c 100644 --- a/src/app/components/how-it-works/bottom/components/how-to-mint.tsx +++ b/src/app/components/how-it-works/bottom/components/how-to-mint.tsx @@ -4,6 +4,7 @@ import { useNavigate } from 'react-router-dom'; import { Box, Button, Image, Text } from '@chakra-ui/react'; import { IntroVideo } from '@components/how-it-works/top/components/intro-video'; import { mintUnmintActions } from '@store/slices/mintunmint/mintunmint.actions'; +import { MintSteps } from '@store/slices/mintunmint/mintunmint.slice'; import { CustomCard } from '../../components/custom-card'; import { FlowStep } from './flow-step'; @@ -77,7 +78,7 @@ export function HowToMint(): React.JSX.Element { + ); } diff --git a/src/app/components/mint-unmint/components/unmint/components/unmint-vault-selector.tsx b/src/app/components/mint-unmint/components/unmint/components/unmint-vault-selector.tsx index 63ae7a42..445ffe6a 100644 --- a/src/app/components/mint-unmint/components/unmint/components/unmint-vault-selector.tsx +++ b/src/app/components/mint-unmint/components/unmint/components/unmint-vault-selector.tsx @@ -6,6 +6,7 @@ import { VaultsListGroupContainer } from '@components/vaults-list/components/vau import { VaultsList } from '@components/vaults-list/vaults-list'; import { VaultContext } from '@providers/vault-context-provider'; import { RootState } from '@store/index'; +import { VaultState } from 'dlc-btc-lib/models'; import { BurnTokenTransactionForm } from '../../burn-transaction-screen/burn-transaction-screen'; @@ -26,7 +27,7 @@ export function UnmintVaultSelector({ return ( <> - {unmintStep[1] ? ( + {unmintStep.vault ? ( Select vault to withdraw Bitcoin: - - + + )} diff --git a/src/app/components/mint-unmint/components/unmint/components/withdraw-screen.tsx b/src/app/components/mint-unmint/components/unmint/components/withdraw-screen.tsx index 392e4792..3327bace 100644 --- a/src/app/components/mint-unmint/components/unmint/components/withdraw-screen.tsx +++ b/src/app/components/mint-unmint/components/unmint/components/withdraw-screen.tsx @@ -9,9 +9,9 @@ import { BitcoinWalletContextState, } from '@providers/bitcoin-wallet-context-provider'; import { ProofOfReserveContext } from '@providers/proof-of-reserve-context-provider'; -import { VaultContext } from '@providers/vault-context-provider'; import { RootState } from '@store/index'; import { mintUnmintActions } from '@store/slices/mintunmint/mintunmint.actions'; +import { RedeemSteps } from '@store/slices/mintunmint/mintunmint.slice'; import { modalActions } from '@store/slices/modal/modal.actions'; interface WithdrawScreenProps { @@ -29,10 +29,10 @@ export function WithdrawScreen({ const { bitcoinWalletContextState, resetBitcoinWalletContext } = useContext(BitcoinWalletContext); const { bitcoinPrice, depositLimit } = useContext(ProofOfReserveContext); - const { allVaults } = useContext(VaultContext); const { unmintStep } = useSelector((state: RootState) => state.mintunmint); - const currentVault = allVaults.find(vault => vault.uuid === unmintStep[1]); + + const currentVault = unmintStep.vault; const [isSubmitting, setIsSubmitting] = useState(false); @@ -60,7 +60,7 @@ export function WithdrawScreen({ function handleCancel() { resetBitcoinWalletContext(); - dispatch(mintUnmintActions.setUnmintStep([0, ''])); + dispatch(mintUnmintActions.setUnmintStep({ step: RedeemSteps.BURN, vault: undefined })); } async function handleButtonClick(assetAmount: number) { @@ -75,7 +75,7 @@ export function WithdrawScreen({ state.mintunmint); const { risk, fetchUserAddressRisk, isLoading } = useRisk(); return ( - + - - {[0].includes(unmintStep[0]) && ( + + {[0].includes(unmintStep.step) && ( )} - {[1, 2].includes(unmintStep[0]) && ( + {[1, 2].includes(unmintStep.step) && ( - - Initiate a Vault on the blockchain and confirm it in your{' '} - - Ethereum Wallet - - . - + {networkType === NetworkType.EVM ? ( + + Initiate a Vault on the blockchain and confirm it in your{' '} + + Ethereum Wallet + + . + + ) : ( + + Initiate a Setup Vault request. If the TrustLine is not yet established, sign the + Set TrustLine Transaction in your wallet. Then, wait for the Attestors to confirm + your request and set up the Vault on the blockchain. + + )} ); @@ -45,20 +60,12 @@ export function Walkthrough({ flow, currentStep }: WalkthroughProps): React.JSX. Enter the Bitcoin amount you wish to deposit into the vault, then verify the - transaction through your{' '} - - Bitcoin Wallet{' '} - - which will lock your Bitcoin on-chain. You will receive equivalent amount of dlcBTC. + transaction through your Bitcoin Wallet which will lock your Bitcoin on-chain. You + will receive equivalent amount of dlcBTC. ); @@ -68,32 +75,33 @@ export function Walkthrough({ flow, currentStep }: WalkthroughProps): React.JSX. - Wait for Bitcoin to get locked on chain{' '} - - (~1 hour) - - . After 6 confirmations, dlcBTC tokens will appear in your Ethereum Wallet. - - - To ensure your dlcBTC tokens - are visible - simply add them - to your Ethereum Wallet. + Wait for Bitcoin to get locked on chain (~1 hour). After 6 confirmations, dlcBTC + tokens will appear in your Wallet. - + {networkType === NetworkType.EVM && ( + <> + + To ensure your dlcBTC tokens + are visible + simply add them + to your Ethereum Wallet. + + + + + )} ); default: @@ -102,7 +110,7 @@ export function Walkthrough({ flow, currentStep }: WalkthroughProps): React.JSX. ); @@ -115,12 +123,19 @@ export function Walkthrough({ flow, currentStep }: WalkthroughProps): React.JSX. - - Select the dlcBTC vault you would like to withdraw from. Burn the desired amount of - dlcBTC to receive the equivalent amount of BTC. - + {networkType === NetworkType.EVM ? ( + + Select the dlcBTC vault you would like to withdraw from. Burn the desired amount + of dlcBTC to receive the equivalent amount of BTC. + + ) : ( + + Select the dlcBTC vault you would like to withdraw from. Sign a check with the + desired amount of dlcBTC to receive the equivalent amount of BTC. + + )} ); case 1: @@ -129,7 +144,7 @@ export function Walkthrough({ flow, currentStep }: WalkthroughProps): React.JSX. {`Once the dlcBTC has been burned, you can withdraw an `} @@ -146,7 +161,7 @@ export function Walkthrough({ flow, currentStep }: WalkthroughProps): React.JSX. After a successful withdraw ( @@ -161,7 +176,7 @@ export function Walkthrough({ flow, currentStep }: WalkthroughProps): React.JSX. ); diff --git a/src/app/components/modals/components/modal-container.tsx b/src/app/components/modals/components/modal-container.tsx index 761fa989..84b71b2e 100644 --- a/src/app/components/modals/components/modal-container.tsx +++ b/src/app/components/modals/components/modal-container.tsx @@ -35,18 +35,20 @@ export function ModalContainer(): React.JSX.Element { /> handleClosingModal(() => modalActions.toggleSuccessfulFlowModalVisibility({ vaultUUID: '', + vault: undefined, flow: 'mint', assetAmount: 0, }) ) } - vaultUUID={isSuccesfulFlowModalOpen[1] ? isSuccesfulFlowModalOpen[1] : ''} + vaultUUID={isSuccesfulFlowModalOpen[2] ? isSuccesfulFlowModalOpen[2] : ''} /> void; } -export function SelectWalletMenu({ +export function SelectEthereumWalletMenu({ wagmiConnector, selectedWagmiConnectorID, isConnectWalletPending, isConnectWalletSuccess, handleConnectWallet, -}: SelectWalletMenuProps): React.JSX.Element { +}: SelectEthereumWalletMenuProps): React.JSX.Element { const { id, icon, name } = wagmiConnector; const isThisWalletSelected = selectedWagmiConnectorID === id; diff --git a/src/app/components/modals/select-wallet-modal/components/select-ripple-wallet-menu.tsx b/src/app/components/modals/select-wallet-modal/components/select-ripple-wallet-menu.tsx new file mode 100644 index 00000000..f9c35fa1 --- /dev/null +++ b/src/app/components/modals/select-wallet-modal/components/select-ripple-wallet-menu.tsx @@ -0,0 +1,33 @@ +import { Box, Button, HStack, Image, Text } from '@chakra-ui/react'; +import { XRPWallet, XRPWalletType } from '@models/wallet'; + +interface SelectRippleWalletMenuProps { + rippleWallet: XRPWallet; + handleConnectWallet: (xrpWalletType: XRPWalletType) => void; +} + +export function SelectRippleWalletMenu({ + rippleWallet, + handleConnectWallet, +}: SelectRippleWalletMenuProps): React.JSX.Element { + const { id, icon, name } = rippleWallet; + + return ( + + ); +} diff --git a/src/app/components/modals/select-wallet-modal/select-wallet-modal.tsx b/src/app/components/modals/select-wallet-modal/select-wallet-modal.tsx index ea08d08d..8d94aeef 100644 --- a/src/app/components/modals/select-wallet-modal/select-wallet-modal.tsx +++ b/src/app/components/modals/select-wallet-modal/select-wallet-modal.tsx @@ -1,56 +1,147 @@ -import { useEffect, useState } from 'react'; +import { useContext, useEffect, useState } from 'react'; import { CheckIcon } from '@chakra-ui/icons'; -import { HStack, ScaleFade, Text, VStack } from '@chakra-ui/react'; +import { HStack, ScaleFade, Tab, TabList, Tabs, Text, VStack, useToast } from '@chakra-ui/react'; import { ModalComponentProps } from '@components/modals/components/modal-container'; import { ModalLayout } from '@components/modals/components/modal.layout'; -import { SelectWalletMenu } from '@components/modals/select-wallet-modal/components/select-wallet-menu'; import { SelectNetworkButton } from '@components/select-network-button/select-network-button'; +import { TransactionScreenWalletInformation } from '@components/transaction-screen/transaction-screen.transaction-form/components/transaction-screen.transaction-form/components/transaction-screen.transaction-form.wallet-information'; +import { useGemWallet } from '@hooks/use-xrpl-gem'; +import { useXRPLLedger } from '@hooks/use-xrpl-ledger'; +import { RippleNetworkID } from '@models/ripple.models'; +import { XRPWalletType, xrpWallets } from '@models/wallet'; +import { NetworkConfigurationContext } from '@providers/network-configuration.provider'; +import { RippleNetworkConfigurationContext } from '@providers/ripple-network-configuration.provider'; +import { XRPWalletContext, XRPWalletContextState } from '@providers/xrp-wallet-context-provider'; +import { EthereumNetworkID } from 'dlc-btc-lib/models'; import { delay } from 'dlc-btc-lib/utilities'; -import { Chain } from 'viem'; import { Connector, useConfig, useConnect } from 'wagmi'; +import { NetworkType } from '@shared/constants/network.constants'; + +import { SelectEthereumWalletMenu } from './components/select-ethereum-wallet-menu'; +import { SelectRippleWalletMenu } from './components/select-ripple-wallet-menu'; + +function formatErrorMessage(error: string): string { + if (error.includes('0x6985')) { + return 'Action Rejected by User'; + } else if (error.includes('0x5515')) { + return 'Locked Device'; + } else { + return error; + } +} + export function SelectWalletModal({ isOpen, handleClose }: ModalComponentProps): React.JSX.Element { + const toast = useToast(); const { connect, isPending, isSuccess, connectors } = useConnect(); - const { chains } = useConfig(); + const { chains: ethereumNetworks } = useConfig(); - const [selectedEthereumNetwork, setSelectedEthereumNetwork] = useState( - undefined + const { setNetworkType } = useContext(NetworkConfigurationContext); + const { + setXRPWalletType, + setXRPWalletContextState, + setUserAddress, + xrpWalletContextState, + setXRPHandler, + } = useContext(XRPWalletContext); + const { connectLedgerWallet, isLoading } = useXRPLLedger(); + const { connectGemWallet } = useGemWallet(); + const { enabledRippleNetworks } = useContext(RippleNetworkConfigurationContext); + + const ethereumNetworkIDs = ethereumNetworks.map( + ethereumNetwork => ethereumNetwork.id.toString() as EthereumNetworkID ); + const rippleNetworkIDs = enabledRippleNetworks.map(rippleNetwork => rippleNetwork.id); + + const [selectedNetworkType, setSelectedNetworkType] = useState(NetworkType.EVM); + + const [selectedNetworkID, setSelectedNetworkID] = useState< + EthereumNetworkID | RippleNetworkID | undefined + >(undefined); + const [selectedWagmiConnectorID, setSelectedWagmiConnectorID] = useState( undefined ); useEffect(() => { - if (isSuccess) { + if (isSuccess || xrpWalletContextState === XRPWalletContextState.READY) { void handleCloseAfterSuccess(); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isSuccess]); + }, [isSuccess, xrpWalletContextState]); async function handleCloseAfterSuccess() { await delay(1000); - setSelectedEthereumNetwork(undefined); + setSelectedNetworkID(undefined); setSelectedWagmiConnectorID(undefined); - if (selectedWagmiConnectorID && selectedWagmiConnectorID !== 'walletConnect') handleClose(); + if (selectedWagmiConnectorID !== 'walletConnect') handleClose(); + setSelectedNetworkType(NetworkType.EVM); } - async function handleConnectWallet(wagmiConnector: Connector) { + async function handleConnectEthereumWallet(wagmiConnector: Connector) { + if (selectedNetworkID === undefined) return; setSelectedWagmiConnectorID(wagmiConnector.id); - connect({ chainId: selectedEthereumNetwork?.id, connector: wagmiConnector }); + connect({ chainId: Number(selectedNetworkID), connector: wagmiConnector }); + setNetworkType(NetworkType.EVM); if (wagmiConnector.id === 'walletConnect') { handleClose(); } } - const handleChangeNetwork = (ethereumNetwork: Chain) => { - setSelectedEthereumNetwork(ethereumNetwork); + async function handleConnectGemWallet() { + setNetworkType(NetworkType.XRPL); + setXRPWalletType(XRPWalletType.Gem); + + const { xrpHandler, userAddress } = await connectGemWallet(); + + setXRPHandler(xrpHandler); + setUserAddress(userAddress); + setXRPWalletContextState(XRPWalletContextState.READY); + } + + async function handleConnectLedgerWallet() { + try { + setNetworkType(NetworkType.XRPL); + setXRPWalletType(XRPWalletType.Ledger); + + const { xrpHandler, userAddress } = await connectLedgerWallet("44'/144'/0'/0/0"); + + setXRPHandler(xrpHandler); + setUserAddress(userAddress); + setXRPWalletContextState(XRPWalletContextState.READY); + } catch (error: any) { + toast({ + title: 'Failed to connect Ledger Wallet', + description: error instanceof Error ? formatErrorMessage(error.message) : '', + status: 'error', + duration: 9000, + isClosable: true, + }); + } + } + + async function handleConnectRippleWallet(xrpWalletType: XRPWalletType) { + switch (xrpWalletType) { + case XRPWalletType.Gem: + await handleConnectGemWallet(); + break; + case XRPWalletType.Ledger: + await handleConnectLedgerWallet(); + break; + default: + break; + } + } + + const handleChangeNetwork = (networkID: EthereumNetworkID | RippleNetworkID) => { + setSelectedNetworkID(networkID); }; return ( handleClose()}> - {!selectedEthereumNetwork ? ( + {!selectedNetworkID ? ( Select Network ) : ( @@ -58,16 +149,29 @@ export function SelectWalletModal({ isOpen, handleClose }: ModalComponentProps): )} + { + setSelectedNetworkType(index === 0 ? NetworkType.EVM : NetworkType.XRPL); + setSelectedNetworkID(undefined); + }} + > + + Ethereum + XRPL + + - + {!isSuccess ? ( Select Wallet @@ -77,16 +181,25 @@ export function SelectWalletModal({ isOpen, handleClose }: ModalComponentProps): )} - {connectors.map(wagmiConnector => ( - - ))} + {selectedNetworkType === NetworkType.EVM + ? connectors.map(wagmiConnector => ( + + )) + : xrpWallets.map(rippleWallet => ( + + ))} + diff --git a/src/app/components/modals/successful-flow-modal/successful-flow-modal.tsx b/src/app/components/modals/successful-flow-modal/successful-flow-modal.tsx index bf54afd7..acc305f3 100644 --- a/src/app/components/modals/successful-flow-modal/successful-flow-modal.tsx +++ b/src/app/components/modals/successful-flow-modal/successful-flow-modal.tsx @@ -1,15 +1,14 @@ -import { useContext } from 'react'; - import { HStack, Text, VStack } from '@chakra-ui/react'; import { TransactionFormNavigateButtonGroup } from '@components/transaction-screen/transaction-screen.transaction-form/components/transaction-screen.transaction-form/components/transaction-screen.transaction-form.navigate-button-group'; import { Vault } from '@components/vault/vault'; -import { VaultContext } from '@providers/vault-context-provider'; +import { Vault as VaultModel } from '@models/vault'; import { ModalComponentProps } from '../components/modal-container'; import { ModalVaultLayout } from '../components/modal.vault.layout'; interface SuccessfulFlowModalProps extends ModalComponentProps { vaultUUID: string; + vault: VaultModel; flow: 'mint' | 'burn'; assetAmount: number; } @@ -25,14 +24,10 @@ function getModalText(flow: 'mint' | 'burn', assetAmount?: number): string { export function SuccessfulFlowModal({ isOpen, handleClose, - vaultUUID, + vault, flow, assetAmount, }: SuccessfulFlowModalProps): React.JSX.Element { - const { allVaults } = useContext(VaultContext); - - const currentVault = allVaults.find(vault => vault.uuid === vaultUUID); - return ( handleClose()}> @@ -41,8 +36,8 @@ export function SuccessfulFlowModal({ {getModalText(flow, assetAmount)} - - + + ); diff --git a/src/app/components/my-vaults-small/my-vaults-small.tsx b/src/app/components/my-vaults-small/my-vaults-small.tsx index 54fbe30c..be0bb3b5 100644 --- a/src/app/components/my-vaults-small/my-vaults-small.tsx +++ b/src/app/components/my-vaults-small/my-vaults-small.tsx @@ -4,6 +4,7 @@ import { useNavigate } from 'react-router-dom'; import { Button, Skeleton } from '@chakra-ui/react'; import { VaultsList } from '@components/vaults-list/vaults-list'; import { VaultContext } from '@providers/vault-context-provider'; +import { VaultState } from 'dlc-btc-lib/models'; import { VaultsListGroupContainer } from '../vaults-list/components/vaults-list-group-container'; import { MyVaultsSmallLayout } from './components/my-vaults-small.layout'; @@ -11,30 +12,38 @@ import { MyVaultsSmallLayout } from './components/my-vaults-small.layout'; export function MyVaultsSmall(): React.JSX.Element { const navigate = useNavigate(); - const vaultContext = useContext(VaultContext); - const { - readyVaults, - pendingVaults, - fundedVaults, - closingVaults, - closedVaults, - isLoading, - allVaults, - } = vaultContext; + const { readyVaults, pendingVaults, fundedVaults, closingVaults, closedVaults, allVaults } = + useContext(VaultContext); return ( - 0} - > - - - - - - + 0}> + + + + + + diff --git a/src/app/components/vault/components/vault.detaills/vault.details.tsx b/src/app/components/vault/components/vault.detaills/vault.details.tsx index 4eb08e74..92dee556 100644 --- a/src/app/components/vault/components/vault.detaills/vault.details.tsx +++ b/src/app/components/vault/components/vault.detaills/vault.details.tsx @@ -1,8 +1,11 @@ +import { useContext } from 'react'; import { useDispatch } from 'react-redux'; import { useNavigate } from 'react-router-dom'; import { Collapse, Stack, VStack } from '@chakra-ui/react'; +import { VaultContext } from '@providers/vault-context-provider'; import { mintUnmintActions } from '@store/slices/mintunmint/mintunmint.actions'; +import { MintSteps, RedeemSteps } from '@store/slices/mintunmint/mintunmint.slice'; import { VaultState } from 'dlc-btc-lib/models'; import { VaultExpandedInformationButtonGroup } from './components/vault.details.button-group/vault.details.button-group'; @@ -17,6 +20,7 @@ interface VaultDetailsProps { vaultFundingTX?: string; vaultWithdrawDepositTX?: string; variant?: 'select' | 'selected'; + handleClose?: () => void; } export function VaultDetails({ @@ -28,30 +32,40 @@ export function VaultDetails({ vaultTotalMintedValue, isVaultExpanded, variant, + handleClose, }: VaultDetailsProps): React.JSX.Element { const navigate = useNavigate(); const dispatch = useDispatch(); + const { allVaults } = useContext(VaultContext); + const vault = allVaults.find(vault => vault.uuid === vaultUUID); + function handleDepositClick() { navigate('/mint-withdraw'); - dispatch(mintUnmintActions.setMintStep([1, vaultUUID])); + dispatch(mintUnmintActions.setMintStep({ step: MintSteps.DEPOSIT, vault: vault })); + if (handleClose) { + handleClose(); + } } function handleWithdrawClick() { navigate('/mint-withdraw'); if (vaultTotalLockedValue === vaultTotalMintedValue) { - dispatch(mintUnmintActions.setUnmintStep([0, vaultUUID])); + dispatch(mintUnmintActions.setUnmintStep({ step: RedeemSteps.BURN, vault: vault })); } else { - dispatch(mintUnmintActions.setUnmintStep([1, vaultUUID])); + dispatch(mintUnmintActions.setUnmintStep({ step: RedeemSteps.WITHDRAW, vault: vault })); + } + if (handleClose) { + handleClose(); } } function handleResumeClick() { navigate('/mint-withdraw'); if (vaultTotalLockedValue === vaultTotalMintedValue) { - dispatch(mintUnmintActions.setMintStep([2, vaultUUID])); + dispatch(mintUnmintActions.setMintStep({ step: MintSteps.PENDING, vault: vault })); } else { - dispatch(mintUnmintActions.setUnmintStep([2, vaultUUID])); + dispatch(mintUnmintActions.setUnmintStep({ step: RedeemSteps.PENDING, vault: vault })); } } diff --git a/src/app/components/vault/vault.tsx b/src/app/components/vault/vault.tsx index 36d819c5..142e457b 100644 --- a/src/app/components/vault/vault.tsx +++ b/src/app/components/vault/vault.tsx @@ -15,16 +15,17 @@ import { VaultProgressBar } from './components/vault.progress-bar'; interface VaultProps { vault: VaultModel; variant?: 'select' | 'selected'; + handleClose?: () => void; } -export function Vault({ vault, variant }: VaultProps): React.JSX.Element { +export function Vault({ vault, variant, handleClose }: VaultProps): React.JSX.Element { const dispatch = useDispatch(); const [isVaultExpanded, setIsVaultExpanded] = useState(false); function handleMainButtonClick() { if (variant === 'select') { const step = vault.valueLocked === vault.valueMinted ? 0 : 1; - dispatch(mintUnmintActions.setUnmintStep([step, vault.uuid])); + dispatch(mintUnmintActions.setUnmintStep([step, vault.uuid, vault])); } else { setIsVaultExpanded(!isVaultExpanded); } @@ -55,6 +56,7 @@ export function Vault({ vault, variant }: VaultProps): React.JSX.Element { isVaultExpanded={isVaultExpanded} vaultFundingTX={vault.fundingTX} vaultWithdrawDepositTX={vault.withdrawDepositTX} + handleClose={handleClose} /> diff --git a/src/app/components/vaults-list/components/vaults-list-group-container.tsx b/src/app/components/vaults-list/components/vaults-list-group-container.tsx index ea4d1f60..84b4fe31 100644 --- a/src/app/components/vaults-list/components/vaults-list-group-container.tsx +++ b/src/app/components/vaults-list/components/vaults-list-group-container.tsx @@ -1,10 +1,17 @@ +import { useContext } from 'react'; + import { Button, HStack, Image, Spinner, Text, VStack } from '@chakra-ui/react'; import { Vault } from '@components/vault/vault'; import { useAddToken } from '@hooks/use-add-token'; import { Vault as VaultModel } from '@models/vault'; +import { NetworkConfigurationContext } from '@providers/network-configuration.provider'; +import { VaultState } from 'dlc-btc-lib/models'; + +import { NetworkType } from '@shared/constants/network.constants'; interface VaultsListGroupContainerProps { label?: string; + vaultState?: VaultState; variant?: 'select'; vaults: VaultModel[]; selectedVaultUUID?: string; @@ -14,10 +21,12 @@ interface VaultsListGroupContainerProps { export function VaultsListGroupContainer({ label, + vaultState, variant, vaults, }: VaultsListGroupContainerProps): React.JSX.Element | boolean { const addToken = useAddToken(); + const { networkType } = useContext(NetworkConfigurationContext); if (vaults.length === 0) return false; @@ -29,7 +38,7 @@ export function VaultsListGroupContainer({ {['Pending'].includes(label) && } {label} - {label === 'Minted dlcBTC' && ( + {vaultState === VaultState.FUNDED && networkType === NetworkType.EVM && (