diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8f7e7ada..f8c6b0a5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -8,7 +8,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 22 - name: Install dependencies run: npm install -g yarn && yarn - name: Install Playwright Browsers diff --git a/.nvmrc b/.nvmrc index 9a2a0e21..53d1c14d 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v20 +v22 diff --git a/Dockerfile b/Dockerfile index dc0cdaed..8740a804 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:20 +FROM node:22 ENV NEXT_TELEMETRY_DISABLED 1 ENV PORT 80 diff --git a/package.json b/package.json index 3aacdfe2..5ae8ed3c 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "author": "Stellar Development Foundation ", "license": "Apache-2.0", "engines": { - "node": ">=20.0.0" + "node": ">=22.0.0" }, "scripts": { "dev": "export NEXT_PUBLIC_COMMIT_HASH=$(git rev-parse --short HEAD) && next dev", diff --git a/src/app/(sidebar)/account/saved/page.tsx b/src/app/(sidebar)/account/saved/page.tsx index 5546087e..b909748e 100644 --- a/src/app/(sidebar)/account/saved/page.tsx +++ b/src/app/(sidebar)/account/saved/page.tsx @@ -1,20 +1,33 @@ "use client"; import { useCallback, useEffect, useState } from "react"; -import { Alert, Text, Card, Input, Icon, Button } from "@stellar/design-system"; +import { + Alert, + Text, + Card, + Input, + Icon, + Button, + Loader, + Badge, +} from "@stellar/design-system"; import { Box } from "@/components/layout/Box"; import { InputSideElement } from "@/components/InputSideElement"; import { SaveKeypairModal } from "@/components/SaveKeypairModal"; import { SavedItemTimestampAndDelete } from "@/components/SavedItemTimestampAndDelete"; -import { useStore } from "@/store/useStore"; import { localStorageSavedKeypairs } from "@/helpers/localStorageSavedKeypairs"; -import { NetworkOptions } from "@/constants/settings"; +import { arrayItem } from "@/helpers/arrayItem"; +import { getNetworkHeaders } from "@/helpers/getNetworkHeaders"; +import { getNetworkById } from "@/helpers/getNetworkById"; + +import { useStore } from "@/store/useStore"; import { useIsTestingNetwork } from "@/hooks/useIsTestingNetwork"; +import { useFriendBot } from "@/query/useFriendBot"; +import { useAccountInfo } from "@/query/useAccountInfo"; import { NetworkType, SavedKeypair } from "@/types/types"; -import { arrayItem } from "@/helpers/arrayItem"; export default function SavedKeypairs() { const { network, selectNetwork, updateIsDynamicNetworkSelect } = useStore(); @@ -37,79 +50,8 @@ export default function SavedKeypairs() { updateSavedKeypairs(); }, [updateSavedKeypairs]); - const SavedKeypair = ({ keypair }: { keypair: SavedKeypair }) => { - return ( - - { - setCurrentKeypairTimestamp(keypair.timestamp); - }} - icon={} - /> - } - /> - - - - - - - { - const savedKeypairs = localStorageSavedKeypairs.get(); - const indexToUpdate = savedKeypairs.findIndex( - (kp) => kp.timestamp === keypair.timestamp, - ); - - if (indexToUpdate >= 0) { - const updatedList = arrayItem.delete( - savedKeypairs, - indexToUpdate, - ); - - localStorageSavedKeypairs.set(updatedList); - updateSavedKeypairs(); - } - }} - /> - - - ); - }; - - const getNetworkById = (networkId: NetworkType) => { - const newNetwork = NetworkOptions.find((n) => n.id === networkId); + const getAndSetNetwork = (networkId: NetworkType) => { + const newNetwork = getNetworkById(networkId); if (newNetwork) { updateIsDynamicNetworkSelect(true); @@ -132,7 +74,7 @@ export default function SavedKeypairs() { const newNetworkId = network.id === "testnet" ? "futurenet" : "testnet"; - getNetworkById(newNetworkId); + getAndSetNetwork(newNetworkId); }} > {`You must switch your network to ${otherNetworkLabel} in order to see those saved @@ -153,7 +95,27 @@ export default function SavedKeypairs() { {savedKeypairs.length === 0 ? `There are no saved keypairs on ${network.label} network.` : savedKeypairs.map((kp) => ( - + { + const savedKeypairs = localStorageSavedKeypairs.get(); + const indexToUpdate = savedKeypairs.findIndex( + (kp) => kp.timestamp === keypair.timestamp, + ); + + if (indexToUpdate >= 0) { + const updatedList = arrayItem.delete( + savedKeypairs, + indexToUpdate, + ); + + localStorageSavedKeypairs.set(updatedList); + updateSavedKeypairs(); + } + }} + /> ))} @@ -173,7 +135,7 @@ export default function SavedKeypairs() { variant="tertiary" size="sm" onClick={() => { - getNetworkById("futurenet"); + getAndSetNetwork("futurenet"); }} > Switch to Futurenet @@ -183,7 +145,7 @@ export default function SavedKeypairs() { variant="tertiary" size="sm" onClick={() => { - getNetworkById("testnet"); + getAndSetNetwork("testnet"); }} > Switch to Testnet @@ -236,3 +198,161 @@ export default function SavedKeypairs() { ); } + +const SavedKeypairItem = ({ + keypair, + setCurrentKeypairTimestamp, + onDelete, +}: { + keypair: SavedKeypair; + setCurrentKeypairTimestamp: (timestamp: number) => void; + onDelete: (keypair: SavedKeypair) => void; +}) => { + const network = getNetworkById(keypair.network.id); + const publicKey = keypair.publicKey; + const horizonUrl = network?.horizonUrl || ""; + const headers = network ? getNetworkHeaders(network, "horizon") : {}; + + const { + error: friendbotError, + isFetching: isFriendbotFetching, + isLoading: isFriendbotLoading, + isSuccess: isFriendbotSuccess, + refetch: fundWithFriendbot, + } = useFriendBot({ + network: network!, + publicKey: publicKey, + key: { type: "saved" }, + headers, + }); + + const { + isFetching: isAccountFetching, + isLoading: isAccountLoading, + error: accountError, + data: accountInfo, + refetch: fetchAccountInfo, + } = useAccountInfo({ + publicKey, + horizonUrl, + headers, + }); + + useEffect(() => { + if (publicKey && horizonUrl) { + fetchAccountInfo(); + } + }, [publicKey, horizonUrl, fetchAccountInfo]); + + useEffect(() => { + if (isFriendbotSuccess) { + fetchAccountInfo(); + } + }, [fetchAccountInfo, isFriendbotSuccess]); + + const renderAccountData = () => { + if ( + isAccountFetching || + isAccountLoading || + isFriendbotFetching || + isFriendbotLoading + ) { + return ; + } + + if (accountError || friendbotError) { + return ( +
+ {accountError?.message || friendbotError?.message} +
+ ); + } + + if (accountInfo && !accountInfo.isFunded) { + return ( + + ); + } + + if (accountInfo?.isFunded) { + const xlmAsset = accountInfo.details.balances.find( + (b: any) => b.asset_type === "native", + ); + + if (xlmAsset) { + return ( + {`Balance: ${xlmAsset.balance} XLM`} + ); + } + } + + return null; + }; + + return ( + + { + setCurrentKeypairTimestamp(keypair.timestamp); + }} + icon={} + /> + } + /> + + + + + + + + <>{renderAccountData()} + + + onDelete(keypair)} + /> + + + ); +}; diff --git a/src/app/(sidebar)/endpoints/components/SavedEndpointsPage.tsx b/src/app/(sidebar)/endpoints/components/SavedEndpointsPage.tsx index 172814c9..aed52213 100644 --- a/src/app/(sidebar)/endpoints/components/SavedEndpointsPage.tsx +++ b/src/app/(sidebar)/endpoints/components/SavedEndpointsPage.tsx @@ -10,6 +10,8 @@ import { Modal, Text, } from "@stellar/design-system"; +import { stringify } from "lossless-json"; + import { TabView } from "@/components/TabView"; import { Box } from "@/components/layout/Box"; import { InputSideElement } from "@/components/InputSideElement"; @@ -19,13 +21,12 @@ import { PrettyJsonTextarea } from "@/components/PrettyJsonTextarea"; import { SavedItemTimestampAndDelete } from "@/components/SavedItemTimestampAndDelete"; import { CopyJsonPayloadButton } from "@/components/CopyJsonPayloadButton"; -import { NetworkOptions } from "@/constants/settings"; import { Routes } from "@/constants/routes"; import { localStorageSavedEndpointsHorizon } from "@/helpers/localStorageSavedEndpointsHorizon"; import { localStorageSavedRpcMethods } from "@/helpers/localStorageSavedRpcMethods"; import { arrayItem } from "@/helpers/arrayItem"; import { formatTimestamp } from "@/helpers/formatTimestamp"; -import { stringify } from "lossless-json"; +import { getNetworkById } from "@/helpers/getNetworkById"; import { useStore } from "@/store/useStore"; import { Network, @@ -73,7 +74,7 @@ export const SavedEndpointsPage = () => { const getNetworkConfig = ( network: LocalStorageSavedNetwork, ): Network | undefined => { - const defaults = NetworkOptions.find((n) => n.id === network.id); + const defaults = getNetworkById(network.id); switch (network.id) { case "testnet": diff --git a/src/components/NetworkSelector/index.tsx b/src/components/NetworkSelector/index.tsx index b8325425..a4fd2589 100644 --- a/src/components/NetworkSelector/index.tsx +++ b/src/components/NetworkSelector/index.tsx @@ -15,8 +15,9 @@ import { localStorageSavedNetwork } from "@/helpers/localStorageSavedNetwork"; import { delayedAction } from "@/helpers/delayedAction"; import { isEmptyObject } from "@/helpers/isEmptyObject"; import { sanitizeObject } from "@/helpers/sanitizeObject"; +import { getNetworkById } from "@/helpers/getNetworkById"; -import { AnyObject, EmptyObj, Network, NetworkType } from "@/types/types"; +import { AnyObject, EmptyObj, Network } from "@/types/types"; import "./styles.scss"; @@ -216,10 +217,6 @@ export const NetworkSelector = () => { }); }; - const getNetworkById = (networkId: NetworkType) => { - return NetworkOptions.find((op) => op.id === networkId); - }; - const getButtonLabel = () => { if (activeNetwork.id === "custom") { return "Switch to Custom Network"; diff --git a/src/helpers/getNetworkById.ts b/src/helpers/getNetworkById.ts new file mode 100644 index 00000000..1110ce74 --- /dev/null +++ b/src/helpers/getNetworkById.ts @@ -0,0 +1,6 @@ +import { NetworkOptions } from "@/constants/settings"; +import { NetworkType } from "@/types/types"; + +export const getNetworkById = (networkId: NetworkType) => { + return NetworkOptions.find((op) => op.id === networkId); +}; diff --git a/src/query/useAccountInfo.ts b/src/query/useAccountInfo.ts new file mode 100644 index 00000000..9acaec78 --- /dev/null +++ b/src/query/useAccountInfo.ts @@ -0,0 +1,42 @@ +import { NetworkHeaders } from "@/types/types"; +import { useQuery } from "@tanstack/react-query"; + +export const useAccountInfo = ({ + publicKey, + horizonUrl, + headers, +}: { + publicKey: string; + horizonUrl: string; + headers: NetworkHeaders; +}) => { + const query = useQuery({ + queryKey: ["useAccountInfo", publicKey], + queryFn: async () => { + try { + const response = await fetch(`${horizonUrl}/accounts/${publicKey}`, { + headers, + }); + const responseJson = await response.json(); + + if (responseJson.status === 404) { + return { + id: publicKey, + isFunded: false, + }; + } + + return { + id: publicKey, + isFunded: true, + details: responseJson, + }; + } catch (e: any) { + throw `Something went wrong. ${e}`; + } + }, + enabled: false, + }); + + return query; +}; diff --git a/src/query/useFriendBot.ts b/src/query/useFriendBot.ts index 3a63ac1c..2658100e 100644 --- a/src/query/useFriendBot.ts +++ b/src/query/useFriendBot.ts @@ -20,7 +20,7 @@ export const useFriendBot = ({ : "https://friendbot.stellar.org"; const query = useQuery({ - queryKey: ["friendBot", key], + queryKey: ["friendBot", publicKey, key], queryFn: async () => { if (!network.horizonUrl) { throw new Error(`Please use a network that supports Horizon`);