diff --git a/package.json b/package.json index f89f677463e..af590d7e211 100644 --- a/package.json +++ b/package.json @@ -137,11 +137,11 @@ "@leather.io/bitcoin": "0.10.0", "@leather.io/constants": "0.8.2", "@leather.io/crypto": "1.1.0", - "@leather.io/models": "0.10.2", - "@leather.io/query": "2.1.0", + "@leather.io/models": "0.11.0", + "@leather.io/query": "2.2.0", "@leather.io/tokens": "0.7.0", - "@leather.io/ui": "1.9.0", - "@leather.io/utils": "0.11.0", + "@leather.io/ui": "1.9.1", + "@leather.io/utils": "0.11.1", "@ledgerhq/hw-transport-webusb": "6.27.19", "@noble/hashes": "1.4.0", "@noble/secp256k1": "2.1.0", @@ -252,7 +252,7 @@ "@leather.io/eslint-config": "0.6.1", "@leather.io/panda-preset": "0.3.4", "@leather.io/prettier-config": "0.5.0", - "@leather.io/rpc": "2.1.1", + "@leather.io/rpc": "2.1.2", "@ls-lint/ls-lint": "2.2.3", "@mdx-js/loader": "3.0.0", "@pandacss/dev": "0.40.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 28a3535bae4..11bb29c9cf8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -43,20 +43,20 @@ importers: specifier: 1.1.0 version: 1.1.0 '@leather.io/models': - specifier: 0.10.2 - version: 0.10.2 + specifier: 0.11.0 + version: 0.11.0 '@leather.io/query': - specifier: 2.1.0 - version: 2.1.0(@stacks/network@6.13.0(encoding@0.1.13))(encoding@0.1.13)(react@18.3.1) + specifier: 2.2.0 + version: 2.2.0(@stacks/network@6.13.0(encoding@0.1.13))(encoding@0.1.13)(react@18.3.1) '@leather.io/tokens': specifier: 0.7.0 version: 0.7.0 '@leather.io/ui': - specifier: 1.9.0 - version: 1.9.0(@babel/core@7.24.9)(@babel/preset-env@7.24.8(@babel/core@7.24.9))(@swc/core@1.7.0)(@types/react-dom@18.3.0)(@types/react@18.3.3)(encoding@0.1.13)(expo-modules-autolinking@1.11.1)(postcss@8.4.38)(ts-node@10.9.2(@swc/core@1.7.0)(@types/node@20.12.12)(typescript@5.4.5))(typescript@5.4.5) + specifier: 1.9.1 + version: 1.9.1(@babel/core@7.24.9)(@babel/preset-env@7.24.8(@babel/core@7.24.9))(@swc/core@1.7.0)(@types/react-dom@18.3.0)(@types/react@18.3.3)(encoding@0.1.13)(expo-modules-autolinking@1.11.1)(postcss@8.4.38)(ts-node@10.9.2(@swc/core@1.7.0)(@types/node@20.12.12)(typescript@5.4.5))(typescript@5.4.5) '@leather.io/utils': - specifier: 0.11.0 - version: 0.11.0 + specifier: 0.11.1 + version: 0.11.1 '@ledgerhq/hw-transport-webusb': specifier: 6.27.19 version: 6.27.19 @@ -383,8 +383,8 @@ importers: specifier: 0.5.0 version: 0.5.0(@vue/compiler-sfc@3.4.32) '@leather.io/rpc': - specifier: 2.1.1 - version: 2.1.1 + specifier: 2.1.2 + version: 2.1.2 '@ls-lint/ls-lint': specifier: 2.2.3 version: 2.2.3 @@ -2601,41 +2601,56 @@ packages: '@leather.io/bitcoin@0.10.0': resolution: {integrity: sha512-Po5MBZzOBCQ9cc27BXkwzpJ2hPUsD4rVtjHZ7jDX3a3Q/v3jGu1jBgyxTglhIpPU/M6ja+a3eXw0XfOW2oKFVQ==} + '@leather.io/bitcoin@0.10.1': + resolution: {integrity: sha512-rZdilqmVcsMYGG/H+bRT6DNvL65Ivme+W6Ihli+nBRw09gNcqB7z8CD+H1is0QFP84o6mxvKAUbZyucrIJB/Gg==} + '@leather.io/constants@0.8.2': resolution: {integrity: sha512-W4Q8e4H7scLlhnA2UFWFsnQYGu4NPGQmmIuq5N13wvOhzZD9iebrgN9I/VtE1GJwiQmBYdzhJ45EmEAM90zFhA==} '@leather.io/crypto@1.1.0': resolution: {integrity: sha512-iI5skLZN745rdeqishDnoMKxtVdT4rCrHUVox4TOywhqTzCw1aOmMpXXkVIen7fNnVrIjuDP0Ek4LBgzhIiDrw==} + '@leather.io/crypto@1.1.1': + resolution: {integrity: sha512-5hAJk4tdwvHBcUEC6loozFmLnuaxYumcR8eyvS5jZrC8CMSWsCKvk0OHMrAI30AuO4BRhiOR+nQfVK3ws4JGmg==} + '@leather.io/eslint-config@0.6.1': resolution: {integrity: sha512-NLvT7wpDR02jFZdp9g08ja5mRxdz/xCcB/tmXWJL8uDt2l+ebjHqaq45dZroAPm+EciLd0HU3jFfsSA7Txgh9w==} '@leather.io/models@0.10.2': resolution: {integrity: sha512-N4KTT0jApIZphx96/abD0xwA+4A23k+hmPWsf98N2LKxG5ZzYmuzoB5VUlw7hGdwpZFbDW625ALQjjPMPoyG8A==} + '@leather.io/models@0.11.0': + resolution: {integrity: sha512-iKhEno6aVhFKvsnsMKbHQYXIqEGo4TOkJcPs/4yHq2EZmPEMFk8FMxUNZZKcKnVSxedInjzFaFhYF/sz+tTxjQ==} + '@leather.io/panda-preset@0.3.4': resolution: {integrity: sha512-q+ri1ObAPe0GsYWKZe/5AWjSwH1lEgRnBn5NIVE7OJUt8eXa1vQuXMeXFLmY+UU7dhVYSY148RU+SNiRl1DfSw==} '@leather.io/prettier-config@0.5.0': resolution: {integrity: sha512-Pul+4MAyBKnQvqgcKJLbZl4DHnS4kCJzSuaYFW6cfHdre7BFn/iY6Er/Dvm9F8g7VMtkdYu68jEYxQ1Xc7A0KQ==} - '@leather.io/query@2.1.0': - resolution: {integrity: sha512-iMM47shEtq4dVx/MlwQInv+oJk9s9dAXDiO3j56QT3HBnOzguStrpgEO5eXvsnASx6xQwguh5xheA4S/DBwhYg==} + '@leather.io/query@2.2.0': + resolution: {integrity: sha512-1JkMXzQcSQNmpXKvSlhBqksFy2lwlZAD+xPsFEoywrgpHm2WDkvLSd7XUuwmB8+VAi9OLqb2Ad+hG3bHUWj0hg==} peerDependencies: react: '*' '@leather.io/rpc@2.1.1': resolution: {integrity: sha512-rAoPxiooffpbF9mtpSleA8Y5O9WZS6VEvGFYDLQPkt3JxAEm0+cVykDYypJWbrnMAqKNL3FVSSnR2TDtsZ8GiA==} + '@leather.io/rpc@2.1.2': + resolution: {integrity: sha512-Smv00bOQF3Suju3peJWAywpFjTurLg+GZgKi7uymKHfkXhnrUmOxYkXKE1azac8E4MardseZIP7jjGU/hgmFnQ==} + '@leather.io/tokens@0.7.0': resolution: {integrity: sha512-RLF8enOE+t/KdcTjjyQUf6rMEyxLb/PPMYZgvoT/FWoQgnjYluhG6zc1HROu7wM8XEBF4kESABIM6UYKtkAs1Q==} - '@leather.io/ui@1.9.0': - resolution: {integrity: sha512-KNfjqsULmj9S697oh4eGkmGBNiEDXl3vH9Ghddv+RcN2IwuWA8ulNbp8C268HyI/PWhBdm3sbM2LMRXpf9qDSA==} + '@leather.io/ui@1.9.1': + resolution: {integrity: sha512-MNkX7rUcUVIxlHZM7j//tZzabRNAzfMU/G83R5jn+Z814JTnMTxHz46Qkw3wbZmqu3OW3+8/gofIxTWgAkczsg==} '@leather.io/utils@0.11.0': resolution: {integrity: sha512-/i1auGETc6WlPCdukbkMKtMUsliDQwUZ/YNJ8noCjXkZTGKvecvH8VXt+nY3lnqUSAigCfSTMk/c8CGLFUJkxQ==} + '@leather.io/utils@0.11.1': + resolution: {integrity: sha512-uRwWihcI0U/QUCheAr9TwKa9NvUb7HB5vtIpiCpYKDeGmjer4lvvIcPvyPxm5G0/ccB/umtqni9nK5EtwJJ6vA==} + '@ledgerhq/devices@8.4.0': resolution: {integrity: sha512-TUrMlWZJ+5AFp2lWMw4rGQoU+WtjIqlFX5SzQDL9phaUHrt4TFierAGHsaj5+tUHudhD4JhIaLI2cn1NOyq5NQ==} @@ -17226,6 +17241,28 @@ snapshots: transitivePeerDependencies: - encoding + '@leather.io/bitcoin@0.10.1(encoding@0.1.13)': + dependencies: + '@bitcoinerlab/secp256k1': 1.0.2 + '@leather.io/constants': 0.8.2 + '@leather.io/crypto': 1.1.1 + '@leather.io/models': 0.11.0 + '@leather.io/utils': 0.11.1 + '@noble/hashes': 1.4.0 + '@noble/secp256k1': 2.1.0 + '@scure/base': 1.1.6 + '@scure/bip32': 1.4.0 + '@scure/bip39': 1.3.0 + '@scure/btc-signer': 1.3.2 + '@stacks/common': 6.13.0 + '@stacks/transactions': 6.15.0(encoding@0.1.13) + bip32: 4.0.0 + bitcoinjs-lib: 6.1.5 + ecpair: 2.1.0 + varuint-bitcoin: 1.1.2 + transitivePeerDependencies: + - encoding + '@leather.io/constants@0.8.2': {} '@leather.io/crypto@1.1.0': @@ -17234,6 +17271,12 @@ snapshots: '@scure/bip32': 1.4.0 '@scure/bip39': 1.3.0 + '@leather.io/crypto@1.1.1': + dependencies: + '@leather.io/utils': 0.11.1 + '@scure/bip32': 1.4.0 + '@scure/bip39': 1.3.0 + '@leather.io/eslint-config@0.6.1(typescript@5.4.5)': dependencies: '@typescript-eslint/eslint-plugin': 6.9.0(@typescript-eslint/parser@6.9.0(eslint@8.56.0)(typescript@5.4.5))(eslint@8.56.0)(typescript@5.4.5) @@ -17249,6 +17292,12 @@ snapshots: '@stacks/stacks-blockchain-api-types': 7.8.2 bignumber.js: 9.1.2 + '@leather.io/models@0.11.0': + dependencies: + '@stacks/stacks-blockchain-api-types': 7.8.2 + bignumber.js: 9.1.2 + zod: 3.23.6 + '@leather.io/panda-preset@0.3.4(jsdom@22.1.0)(typescript@5.4.5)': dependencies: '@pandacss/dev': 0.40.1(jsdom@22.1.0)(typescript@5.4.5) @@ -17264,15 +17313,15 @@ snapshots: - '@vue/compiler-sfc' - supports-color - '@leather.io/query@2.1.0(@stacks/network@6.13.0(encoding@0.1.13))(encoding@0.1.13)(react@18.3.1)': + '@leather.io/query@2.2.0(@stacks/network@6.13.0(encoding@0.1.13))(encoding@0.1.13)(react@18.3.1)': dependencies: '@fungible-systems/zone-file': 2.0.0 '@hirosystems/token-metadata-api-client': 1.2.0(encoding@0.1.13) - '@leather.io/bitcoin': 0.10.0(encoding@0.1.13) + '@leather.io/bitcoin': 0.10.1(encoding@0.1.13) '@leather.io/constants': 0.8.2 - '@leather.io/models': 0.10.2 - '@leather.io/rpc': 2.1.1 - '@leather.io/utils': 0.11.0 + '@leather.io/models': 0.11.0 + '@leather.io/rpc': 2.1.2 + '@leather.io/utils': 0.11.1 '@noble/hashes': 1.4.0 '@scure/base': 1.1.6 '@scure/bip32': 1.4.0 @@ -17302,13 +17351,18 @@ snapshots: '@leather.io/models': 0.10.2 zod: 3.23.6 + '@leather.io/rpc@2.1.2': + dependencies: + '@leather.io/models': 0.11.0 + zod: 3.23.6 + '@leather.io/tokens@0.7.0': {} - '@leather.io/ui@1.9.0(@babel/core@7.24.9)(@babel/preset-env@7.24.8(@babel/core@7.24.9))(@swc/core@1.7.0)(@types/react-dom@18.3.0)(@types/react@18.3.3)(encoding@0.1.13)(expo-modules-autolinking@1.11.1)(postcss@8.4.38)(ts-node@10.9.2(@swc/core@1.7.0)(@types/node@20.12.12)(typescript@5.4.5))(typescript@5.4.5)': + '@leather.io/ui@1.9.1(@babel/core@7.24.9)(@babel/preset-env@7.24.8(@babel/core@7.24.9))(@swc/core@1.7.0)(@types/react-dom@18.3.0)(@types/react@18.3.3)(encoding@0.1.13)(expo-modules-autolinking@1.11.1)(postcss@8.4.38)(ts-node@10.9.2(@swc/core@1.7.0)(@types/node@20.12.12)(typescript@5.4.5))(typescript@5.4.5)': dependencies: '@expo/vector-icons': 14.0.0 '@leather.io/tokens': 0.7.0 - '@leather.io/utils': 0.11.0 + '@leather.io/utils': 0.11.1 '@radix-ui/react-accessible-icon': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@radix-ui/react-accordion': 1.1.2(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@radix-ui/react-avatar': 1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) @@ -17359,6 +17413,13 @@ snapshots: '@leather.io/rpc': 2.1.1 bignumber.js: 9.1.2 + '@leather.io/utils@0.11.1': + dependencies: + '@leather.io/constants': 0.8.2 + '@leather.io/models': 0.11.0 + '@leather.io/rpc': 2.1.2 + bignumber.js: 9.1.2 + '@ledgerhq/devices@8.4.0': dependencies: '@ledgerhq/errors': 6.17.0 diff --git a/src/app/features/add-network/add-network-form.tsx b/src/app/features/add-network/add-network-form.tsx index 0372ea738e4..913831f1cef 100644 --- a/src/app/features/add-network/add-network-form.tsx +++ b/src/app/features/add-network/add-network-form.tsx @@ -39,7 +39,8 @@ const networks: { ]; export function AddNetworkForm() { - const { handleChange, setFieldValue, values } = useFormikContext(); + const { handleChange, setFieldValue, values, initialValues } = + useFormikContext(); const setStacksUrl = useCallback( (value: string) => { @@ -92,7 +93,7 @@ export function AddNetworkForm() { Bitcoin API { void setFieldValue('bitcoinNetwork', value); }} diff --git a/src/app/features/add-network/use-add-network.tsx b/src/app/features/add-network/use-add-network.tsx index e0aba75008f..c4acb4b18ed 100644 --- a/src/app/features/add-network/use-add-network.tsx +++ b/src/app/features/add-network/use-add-network.tsx @@ -1,9 +1,15 @@ import { useState } from 'react'; -import { useNavigate } from 'react-router-dom'; +import { useLocation, useNavigate } from 'react-router-dom'; import { ChainID } from '@stacks/transactions'; +import z from 'zod'; -import type { BitcoinNetworkModes, DefaultNetworkConfigurations } from '@leather.io/models'; +import { + type BitcoinNetworkModes, + type DefaultNetworkConfigurations, + type NetworkConfiguration, + networkConfigurationSchema, +} from '@leather.io/models'; import { RouteUrls } from '@shared/route-urls'; import { isValidUrl } from '@shared/utils/validate-url'; @@ -39,16 +45,40 @@ const initialFormValues: AddNetworkFormValues = { bitcoinNetwork: 'mainnet', }; +function useInitialValues() { + const { state } = useLocation(); + const network = state.network as NetworkConfiguration | undefined; + + if (!network) { + return initialFormValues; + } + + const isProperStateProvided = networkConfigurationSchema.safeParse(network).success; + + if (!isProperStateProvided) { + return initialFormValues; + } + + return { + key: network.id, + name: network.name, + stacksUrl: network.chain.stacks.url, + bitcoinUrl: network.chain.bitcoin.bitcoinUrl, + bitcoinNetwork: network.chain.bitcoin.bitcoinNetwork, + }; +} + export function useAddNetwork() { const [loading, setLoading] = useState(false); const [error, setError] = useState(''); const navigate = useNavigate(); const network = useCurrentStacksNetworkState(); const networksActions = useNetworksActions(); + const initialValues = useInitialValues(); return { error, - initialFormValues, + initialFormValues: initialValues, loading, onSubmit: async (values: AddNetworkFormValues) => { const { name, stacksUrl, bitcoinUrl, key, bitcoinNetwork } = values; @@ -113,10 +143,16 @@ export function useAddNetwork() { isSubnet && (parentNetworkId === PeerNetworkID.Mainnet || parentNetworkId === PeerNetworkID.Testnet); + function removeEditedNetwork() { + if (initialValues.key) { + networksActions.removeNetwork(initialValues.key); + } + } // Currently, only subnets of mainnet and testnet are supported in the wallet if (isFirstLevelSubnet) { const parentChainId = parentNetworkId === PeerNetworkID.Mainnet ? ChainID.Mainnet : ChainID.Testnet; + removeEditedNetwork(); networksActions.addNetwork({ id: key as DefaultNetworkConfigurations, name: name, @@ -128,6 +164,7 @@ export function useAddNetwork() { }); navigate(RouteUrls.Home); } else if (chainId === ChainID.Mainnet || chainId === ChainID.Testnet) { + removeEditedNetwork(); networksActions.addNetwork({ id: key as DefaultNetworkConfigurations, name: name, diff --git a/src/app/features/settings/network/components/network-list-item-menu.tsx b/src/app/features/settings/network/components/network-list-item-menu.tsx new file mode 100644 index 00000000000..f5f1c74ed1e --- /dev/null +++ b/src/app/features/settings/network/components/network-list-item-menu.tsx @@ -0,0 +1,58 @@ +import { SettingsSelectors } from '@tests/selectors/settings.selectors'; +import { css } from 'leather-styles/css'; +import { HStack, styled } from 'leather-styles/jsx'; + +import { DotsVerticalIcon, DropdownMenu, PenIcon, TrashIcon } from '@leather.io/ui'; + +interface Props { + onEditNetwork(): void; + onClickDeleteNetwork(): void; +} + +export function NetworkItemMenu({ onClickDeleteNetwork, onEditNetwork }: Props) { + return ( + + + + + + + + { + e.stopPropagation(); + onEditNetwork(); + }} + > + + + Edit + + + { + e.stopPropagation(); + onClickDeleteNetwork(); + }} + > + + + Delete + + + + + + + ); +} diff --git a/src/app/features/settings/network/components/network-list-item.layout.tsx b/src/app/features/settings/network/components/network-list-item.layout.tsx index b0b4afef823..b0aea3769be 100644 --- a/src/app/features/settings/network/components/network-list-item.layout.tsx +++ b/src/app/features/settings/network/components/network-list-item.layout.tsx @@ -1,11 +1,13 @@ import { NetworkSelectors } from '@tests/selectors/network.selectors'; import { SettingsSelectors } from '@tests/selectors/settings.selectors'; -import { Flex, Stack, styled } from 'leather-styles/jsx'; +import { Flex, HStack, Stack, styled } from 'leather-styles/jsx'; import type { NetworkConfiguration } from '@leather.io/models'; -import { Button, CheckmarkIcon, CloudOffIcon, TrashIcon } from '@leather.io/ui'; +import { Button, CheckmarkIcon, CloudOffIcon } from '@leather.io/ui'; -import { getUrlHostname } from '@app/common/utils'; +import { getUrlHostname, truncateString } from '@app/common/utils'; + +import { NetworkItemMenu } from './network-list-item-menu'; interface NetworkListItemLayoutProps { networkId: string; @@ -14,8 +16,10 @@ interface NetworkListItemLayoutProps { isCustom: boolean; network: NetworkConfiguration; onSelectNetwork(): void; + onEditNetwork(): void; onRemoveNetwork(id: string): void; } + export function NetworkListItemLayout({ networkId, isOnline, @@ -24,6 +28,7 @@ export function NetworkListItemLayout({ isCustom, onRemoveNetwork, onSelectNetwork, + onEditNetwork, }: NetworkListItemLayoutProps) { const unselectable = !isOnline || isActive; return ( @@ -49,31 +54,28 @@ export function NetworkListItemLayout({ > - - {network.name} - + + {truncateString(network.name, 20)} + {isActive && ( + + )} + + {getUrlHostname(network.chain.stacks.url)} - {!isOnline ? ( - - ) : isActive ? ( - - ) : null} + {!isOnline ? : null} + {isOnline && isCustom && ( + onRemoveNetwork(network.id)} + onEditNetwork={onEditNetwork} + /> + )} - {isCustom && ( - - )} ); diff --git a/src/app/features/settings/network/network-list-item.tsx b/src/app/features/settings/network/network-list-item.tsx index 6134f12b195..3a399d1a4de 100644 --- a/src/app/features/settings/network/network-list-item.tsx +++ b/src/app/features/settings/network/network-list-item.tsx @@ -10,12 +10,14 @@ interface NetworkListItemProps { isCustom: boolean; onNetworkSelected(networkId: string): void; onRemoveNetwork(networkId: string): void; + onEditNetwork(): void; } export function NetworkListItem({ networkId, onNetworkSelected, onRemoveNetwork, isCustom, + onEditNetwork, }: NetworkListItemProps) { const currentNetworkId = useCurrentNetworkId(); const networks = useNetworks(); @@ -31,6 +33,7 @@ export function NetworkListItem({ networkId={networkId} isCustom={isCustom} onSelectNetwork={() => onNetworkSelected(networkId)} + onEditNetwork={onEditNetwork} onRemoveNetwork={onRemoveNetwork} /> ); diff --git a/src/app/features/settings/network/network.tsx b/src/app/features/settings/network/network.tsx index a79ef308ab0..87b3e85b78c 100644 --- a/src/app/features/settings/network/network.tsx +++ b/src/app/features/settings/network/network.tsx @@ -23,7 +23,7 @@ interface NetworkDialogProps { export function NetworkDialog({ onClose }: NetworkDialogProps) { const navigate = useNavigate(); const networks = useNetworks(); - + console.log({ networks }); const networksActions = useNetworksActions(); const currentNetwork = useCurrentNetworkState(); @@ -75,6 +75,14 @@ export function NetworkDialog({ onClose }: NetworkDialogProps) { if (id === currentNetwork.id) networksActions.changeNetwork('mainnet'); removeNetwork(id); }} + onEditNetwork={() => { + onClose(); + navigate(RouteUrls.AddNetwork, { + state: { + network: networks[id], + }, + }); + }} /> ))} diff --git a/src/app/features/settings/settings.tsx b/src/app/features/settings/settings.tsx index 84f8a3e345b..8b99dbb45cc 100644 --- a/src/app/features/settings/settings.tsx +++ b/src/app/features/settings/settings.tsx @@ -27,6 +27,7 @@ import { analytics } from '@shared/utils/analytics'; import { useKeyActions } from '@app/common/hooks/use-key-actions'; import { useModifierKey } from '@app/common/hooks/use-modifier-key'; import { useWalletType } from '@app/common/use-wallet-type'; +import { truncateString } from '@app/common/utils'; import { openInNewTab, openIndexPageInNewTab } from '@app/common/utils/open-in-new-tab'; import { AppVersion } from '@app/components/app-version'; import { Divider } from '@app/components/layout/divider'; @@ -130,7 +131,7 @@ export function Settings({ triggerButton, toggleSwitchAccount }: SettingsProps) Change network - {currentNetworkId} + {truncateString(currentNetworkId.toString(), 15)}