Skip to content

Saved Smart Contract IDs #1392

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
May 9, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 77 additions & 20 deletions src/app/(sidebar)/smart-contracts/contract-explorer/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,33 @@ import { useStore } from "@/store/useStore";
import { useSEContractInfo } from "@/query/external/useSEContractInfo";
import { useWasmGitHubAttestation } from "@/query/useWasmGitHubAttestation";
import { validate } from "@/validate";

import { getNetworkHeaders } from "@/helpers/getNetworkHeaders";
import { localStorageSavedContracts } from "@/helpers/localStorageSavedContracts";
import { buildContractExplorerHref } from "@/helpers/buildContractExplorerHref";
import { delayedAction } from "@/helpers/delayedAction";

import { Box } from "@/components/layout/Box";
import { PageCard } from "@/components/layout/PageCard";
import { MessageField } from "@/components/MessageField";
import { TabView } from "@/components/TabView";
import { SwitchNetworkButtons } from "@/components/SwitchNetworkButtons";
import { PoweredByStellarExpert } from "@/components/PoweredByStellarExpert";
import { ContractInfo } from "./components/ContractInfo";
import { InvokeContract } from "./components/InvokeContract";
import { SaveToLocalStorageModal } from "@/components/SaveToLocalStorageModal";

import { trackEvent, TrackingEvent } from "@/metrics/tracking";

import { ContractInfo } from "./components/ContractInfo";
import { InvokeContract } from "./components/InvokeContract";

export default function ContractExplorer() {
const { network, smartContracts } = useStore();
const { network, smartContracts, savedContractId, clearSavedContractId } =
useStore();

const [contractActiveTab, setContractActiveTab] = useState("contract-info");
const [contractIdInput, setContractIdInput] = useState("");
const [contractIdInputError, setContractIdInputError] = useState("");
const [isSaveModalVisible, setIsSaveModalVisible] = useState(false);

const {
data: contractInfoData,
Expand Down Expand Up @@ -62,9 +71,24 @@ export default function ContractExplorer() {
isWasmFetching;

useEffect(() => {
// Pre-fill on page refresh and initial load
if (smartContracts.explorer.contractId) {
setContractIdInput(smartContracts.explorer.contractId);
}

// Pre-fill only on initial load when navigating from the Saved Smart
// Contract IDs view.
if (savedContractId) {
setContractIdInput(savedContractId);

// Remove temporary savedContractId from URL
delayedAction({
action: () => {
clearSavedContractId();
},
delay: 200,
});
}
// On page load only
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
Expand Down Expand Up @@ -102,7 +126,7 @@ export default function ContractExplorer() {
const renderButtons = () => {
if (isCurrentNetworkSupported) {
return (
<Box gap="sm" direction="row">
<Box gap="sm" direction="row" wrap="wrap">
<Button
size="md"
variant="secondary"
Expand All @@ -115,22 +139,37 @@ export default function ContractExplorer() {

<>
{contractIdInput ? (
<Button
size="md"
variant="error"
icon={<Icon.RefreshCw01 />}
onClick={() => {
resetFetchContractInfo();
setContractIdInput("");

trackEvent(
TrackingEvent.SMART_CONTRACTS_EXPLORER_CLEAR_CONTRACT,
);
}}
disabled={isLoading}
>
Clear
</Button>
<>
<Button
disabled={isLoadContractDisabled || isLoading}
size="md"
variant="tertiary"
icon={<Icon.Save01 />}
onClick={(e) => {
e.preventDefault();
setIsSaveModalVisible(true);
}}
>
Save Contract ID
</Button>

<Button
size="md"
variant="error"
icon={<Icon.RefreshCw01 />}
onClick={() => {
resetFetchContractInfo();
setContractIdInput("");

trackEvent(
TrackingEvent.SMART_CONTRACTS_EXPLORER_CLEAR_CONTRACT,
);
}}
disabled={isLoading}
>
Clear
</Button>
</>
) : null}
</>
</Box>
Expand Down Expand Up @@ -242,6 +281,24 @@ export default function ContractExplorer() {
<PoweredByStellarExpert />
</>
</>

<SaveToLocalStorageModal
type="save"
itemTitle="Smart Contract ID"
itemProps={{
contractId: contractIdInput,
shareableUrl: `${window.location.origin}${buildContractExplorerHref(contractIdInput)}`,
}}
allSavedItems={localStorageSavedContracts.get()}
isVisible={isSaveModalVisible}
onClose={() => {
setIsSaveModalVisible(false);
}}
onUpdate={(updatedItems) => {
localStorageSavedContracts.set(updatedItems);
trackEvent(TrackingEvent.SMART_CONTRACTS_EXPLORER_SAVE);
}}
/>
</Box>
);
}
184 changes: 184 additions & 0 deletions src/app/(sidebar)/smart-contracts/saved/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
"use client";

import { useCallback, useEffect, useState } from "react";
import { Input, Icon, Button } from "@stellar/design-system";
import { useRouter } from "next/navigation";

import { Box } from "@/components/layout/Box";
import { InputSideElement } from "@/components/InputSideElement";
import { SavedItemTimestampAndDelete } from "@/components/SavedItemTimestampAndDelete";
import { PageCard } from "@/components/layout/PageCard";
import { SaveToLocalStorageModal } from "@/components/SaveToLocalStorageModal";
import { ShareUrlButton } from "@/components/ShareUrlButton";

import { localStorageSavedContracts } from "@/helpers/localStorageSavedContracts";
import { arrayItem } from "@/helpers/arrayItem";
import { delayedAction } from "@/helpers/delayedAction";

import { Routes } from "@/constants/routes";
import { useStore } from "@/store/useStore";
import { trackEvent, TrackingEvent } from "@/metrics/tracking";

import { SavedContract } from "@/types/types";

export default function SavedSmartContracts() {
const { network } = useStore();

const [savedContracts, setSavedContracts] = useState<SavedContract[]>([]);
const [currentContractTimestamp, setCurrentContractTimestamp] = useState<
number | undefined
>();

const updateSavedContracts = useCallback(() => {
const contracts = localStorageSavedContracts
.get()
.filter((s) => s.network.id === network.id);
setSavedContracts(contracts);
}, [network.id]);

useEffect(() => {
updateSavedContracts();
}, [updateSavedContracts]);

return (
<Box gap="md">
<PageCard heading="Saved Smart Contract IDs">
<Box gap="md">
<>
{savedContracts.length === 0
? `There are no saved smart contract IDs on ${network.label} network.`
: savedContracts.map((c) => (
<SavedContractItem
key={`saved-contract-${c.timestamp}`}
contract={c}
setCurrentContractTimestamp={setCurrentContractTimestamp}
onDelete={(contract) => {
const savedContracts = localStorageSavedContracts.get();
const indexToUpdate = savedContracts.findIndex(
(c) => c.timestamp === contract.timestamp,
);

if (indexToUpdate >= 0) {
const updatedList = arrayItem.delete(
savedContracts,
indexToUpdate,
);

localStorageSavedContracts.set(updatedList);
updateSavedContracts();
}
}}
/>
))}
</>
</Box>
</PageCard>

<SaveToLocalStorageModal
type="editName"
itemTitle="Smart Contract ID"
itemTimestamp={currentContractTimestamp}
allSavedItems={localStorageSavedContracts.get()}
isVisible={currentContractTimestamp !== undefined}
onClose={(isUpdate?: boolean) => {
setCurrentContractTimestamp(undefined);

if (isUpdate) {
updateSavedContracts();
}
}}
onUpdate={(updatedItems) => {
localStorageSavedContracts.set(updatedItems);
}}
/>
</Box>
);
}

const SavedContractItem = ({
contract,
setCurrentContractTimestamp,
onDelete,
}: {
contract: SavedContract;
setCurrentContractTimestamp: (timestamp: number) => void;
onDelete: (contract: SavedContract) => void;
}) => {
const { setSavedContractId } = useStore();
const router = useRouter();

return (
<Box
gap="sm"
addlClassName="PageBody__content SavedContractItem"
data-testid="saved-contract-item"
>
<Input
id={`saved-contract-${contract.timestamp}-name`}
data-testid="saved-contract-name"
fieldSize="md"
value={contract.name}
readOnly
leftElement="Name"
rightElement={
<InputSideElement
variant="button"
placement="right"
onClick={() => {
setCurrentContractTimestamp(contract.timestamp);
}}
icon={<Icon.Edit05 />}
/>
}
/>

<Input
id={`saved-contract-${contract.timestamp}-id`}
data-testid="saved-contract-id"
fieldSize="md"
value={contract.contractId}
readOnly
leftElement="Contract ID"
copyButton={{ position: "right" }}
/>

<Box
gap="lg"
direction="row"
align="center"
justify="space-between"
addlClassName="Endpoints__urlBar__footer"
>
<Box gap="sm" direction="row">
<Button
size="md"
variant="tertiary"
type="button"
onClick={() => {
// Set a temporary param in URL that we will remove when the route
// loads.
setSavedContractId(contract.contractId);
trackEvent(TrackingEvent.SMART_CONTRACTS_SAVED_VIEW_IN_EXPLORER);

delayedAction({
action: () => {
router.push(Routes.SMART_CONTRACTS_CONTRACT_EXPLORER);
},
delay: 300,
});
}}
>
View in Contract Explorer
</Button>

<ShareUrlButton shareableUrl={contract.shareableUrl} />
</Box>

<SavedItemTimestampAndDelete
timestamp={contract.timestamp}
onDelete={() => onDelete(contract)}
/>
</Box>
</Box>
);
};
8 changes: 8 additions & 0 deletions src/app/(sidebar)/smart-contracts/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,14 @@
}
}

// Saved Smart Contract IDs
.SavedContractItem {
.Input__side-element--left {
width: pxToRem(76px);
}
}


.NoContractLoaded {
background-color: var(--sds-clr-gray-03);
border-radius: pxToRem(8px);
Expand Down
10 changes: 10 additions & 0 deletions src/constants/navItems.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,16 @@ export const XDR_NAV_ITEMS = [
];

export const SMART_CONTRACTS_NAV_ITEMS = [
{
navItems: [
{
route: Routes.SMART_CONTRACTS_SAVED,
label: "Saved Smart Contract IDs",
icon: <Icon.Save03 />,
},
],
hasBottomDivider: true,
},
{
instruction: "Smart Contract Tools",
navItems: [
Expand Down
1 change: 1 addition & 0 deletions src/constants/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ export enum Routes {
SMART_CONTRACTS = "/smart-contracts",
SMART_CONTRACTS_CONTRACT_EXPLORER = "/smart-contracts/contract-explorer",
SMART_CONTRACTS_CONTRACT_LIST = "/smart-contracts/contract-list",
SMART_CONTRACTS_SAVED = "/smart-contracts/saved",
// Blockchain Explorer
BLOCKCHAIN_EXPLORER = "/explorer",
}
1 change: 1 addition & 0 deletions src/constants/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export const LOCAL_STORAGE_SAVED_RPC_METHODS =
export const LOCAL_STORAGE_SAVED_TRANSACTIONS =
"stellar_lab_saved_transactions";
export const LOCAL_STORAGE_SAVED_KEYPAIRS = "stellar_lab_saved_keypairs";
export const LOCAL_STORAGE_SAVED_CONTRACTS = "stellar_lab_saved_contract_ids";
export const LOCAL_STORAGE_SAVED_THEME = "stellarTheme:Laboratory";

export const XDR_TYPE_TRANSACTION_ENVELOPE = "TransactionEnvelope";
Expand Down
Loading