diff --git a/.cargo/config.toml b/.cargo/config.toml index 6beb415..39f8a8f 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,7 +1,7 @@ # paths = ["/path/to/override"] # path dependency overrides [alias] # command aliases -install_soroban = "install --git https://github.com/ahalabs/soroban-tools --rev ad3d6b6da54c47bc7a2343ba484ca173cd48f21f --debug --root ./target soroban-cli" +install_soroban = "install --version 20.3.1 --debug --root ./target soroban-cli" # c = "check" # t = "test" # r = "run" diff --git a/.gitignore b/.gitignore index 3e5f76e..4ea2230 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,7 @@ yarn-error.log* pnpm-debug.log* # local env files +.env .env*.local # environment variables @@ -46,4 +47,8 @@ next-env.d.ts # Cargo/Rust build directory target -.soroban/ \ No newline at end of file +.soroban/ + +# contract clients are auto-built at run/compile time and considered build artifacts +packages/* +!packages/.gitkeep diff --git a/components/dapp-info-popup/index.tsx b/components/dapp-info-popup/index.tsx index ed04590..9cff421 100644 --- a/components/dapp-info-popup/index.tsx +++ b/components/dapp-info-popup/index.tsx @@ -1,7 +1,7 @@ import Popup from 'reactjs-popup'; import styles from './style.module.css'; import { useState, useEffect, ChangeEvent } from 'react'; -import { useThemeContext } from '../ThemeContext' +import { useThemeContext } from '../../context/ThemeContext' export default function PopupDappInfo() { diff --git a/components/deployed-tab/backend.ts b/components/deployed-tab/backend.ts new file mode 100644 index 0000000..71b01ca --- /dev/null +++ b/components/deployed-tab/backend.ts @@ -0,0 +1,192 @@ +import { smartdeploy } from "@/pages"; +import { DeployEventData, ClaimEventData, bumpContractInstance } from '@/mercury_indexer/smartdeploy-api-client'; +import { TimeToLive } from '@/context/TimeToLiveContext' +import { format } from 'date-fns' +import { Ok, Err, Version } from 'smartdeploy-client' +import { WalletContextType } from '@/context/WalletContext' +import { SetStateAction, Dispatch } from "react"; + +export interface DeployedContract { + index: number; + name: string; + address: string; + deployer: string; + fromPublished: string; + version: Version | undefined; +} + +export async function listAllDeployedContracts( + deployEvents: DeployEventData[] | undefined, + claimEvents: ClaimEventData[] | undefined, +) { + + if (deployEvents && claimEvents) { + try { + + ///@ts-ignore + const { result } = await smartdeploy.listDeployedContracts({ start: undefined, limit: undefined }); + const response = result; + + if (response instanceof Ok) { + + let deployedContracts: DeployedContract[] = []; + + const contractArray = response.unwrap(); + + ///@ts-ignore + contractArray.forEach(([name, address], i) => { + + let eventData: DeployEventData | ClaimEventData | undefined = deployEvents.find(event => event.contractId == address); + + // Contract deployed with SmartDeploy => trigger a Deploy event + if (eventData) { + const parsedDeployedContract: DeployedContract = { + index: i, + name: name, + address: address, + deployer: eventData.deployer, + fromPublished: eventData.publishedName, + version: eventData.version + } + + deployedContracts.push(parsedDeployedContract); + } + // If no Deploy events the contract has been claimed + else { + eventData = claimEvents.find(event => event.contractId == address); + const parsedDeployedContract: DeployedContract = { + index: i, + name: name, + address: address, + deployer: eventData!.claimer, + fromPublished: eventData!.wasmHash, + version: undefined + } + + deployedContracts.push(parsedDeployedContract); + } + + }); + + return deployedContracts; + + } else if (response instanceof Err) { + response.unwrap(); + } else { + throw new Error("listDeployedContracts returns undefined. Impossible to fetch the deployed contracts."); + } + } catch (error) { + console.error(error); + window.alert(error); + } + } + else { + return 0; + } +} + +export function getMyDeployedContracts(deployedContracts: DeployedContract[], address: string) { + + const myDeployedContracts: DeployedContract[] = deployedContracts.filter(deployedContract => deployedContract.deployer === address); + return myDeployedContracts; + +} + +export type ImportArgsObj = { + deployed_name: string, + id: string, + owner: string, +}; + +export async function importContract( + walletContext: WalletContextType, + importData: ImportArgsObj, + setDeployedName: Dispatch>, + setContractId: Dispatch>, + setAdmin: Dispatch>, + setIsImporting: Dispatch>, + setBumping: Dispatch>, +) { + + // Check if the Wallet is connected + if (walletContext.address === "") { + alert("Wallet not connected. Please, connect a Stellar account."); + } + // Check is the network is Futurenet + else if (walletContext.network.replace(" ", "").toUpperCase() !== "TESTNET") { + alert("Wrong Network. Please, switch to Testnet."); + } + else { + // Check if deployed name contains spaces + if (importData.deployed_name.includes(' ')) { + alert("Deployed name cannot includes spaces. Please, remove the spaces."); + } + // Check if contract_id contains spaces + if (importData.id.includes(' ')) { + alert("Contract Id cannot includes spaces. Please, remove the spaces."); + } + // Check if admin contains spaces + if (importData.owner.includes(' ')) { + alert("Admin address cannot includes spaces. Please, remove the spaces."); + } + // Now that everything is ok, import the contract + else { + + try { + const tx = await smartdeploy.claimAlreadyDeployedContract(importData); + await tx.signAndSend(); + + setDeployedName(""); + setContractId(""); + setAdmin(""); + setIsImporting(false); + setBumping(null); + + } catch (error) { + console.error(error); + window.alert(error); + return false; + } + + } + + } +} + +export function formatCountDown(initialTime: number) { + + const days = Math.floor(initialTime / (60 * 60 * 24)); + const hours = Math.floor((initialTime % (60 * 60 * 24)) / (60 * 60)); + const minutes = Math.floor((initialTime % (60 * 60)) / 60); + + return `${days}d${hours}h${minutes}m`; +} + +export async function bumpAndQueryNewTtl( + contract_id: String, + ledgers_to_extend: number, + ttl: TimeToLive, + newMap: Map, +) { + try { + const datas = await bumpContractInstance(contract_id, ledgers_to_extend); + if (datas != 0) { + const newTtl = datas as number; + const newTtlSec = newTtl * 5; + const now = new Date(); + const newExpirationDate = format(now.getTime() + newTtlSec * 1000, "MM/dd/yyyy"); + + const newValue = { + ...ttl, + date: newExpirationDate, + ttlSec: newTtlSec, + countdown: formatCountDown(newTtlSec) + } + newMap.set(contract_id, newValue); + + } + } catch (error) { + console.error(error); + window.alert(error); + } +} \ No newline at end of file diff --git a/components/deployed-tab/copy-component.tsx b/components/deployed-tab/copy-component.tsx new file mode 100644 index 0000000..d30845e --- /dev/null +++ b/components/deployed-tab/copy-component.tsx @@ -0,0 +1,66 @@ +import styles from './style.module.css'; + +import { useState, useEffect, Dispatch, SetStateAction } from "react"; +import { FaRegClipboard } from "react-icons/fa"; +import { MdDone } from "react-icons/md" +import { useThemeContext } from '@/context/ThemeContext'; +import Popup from 'reactjs-popup'; + +async function copyAddr(setCopied: Dispatch> , text: string) { + await navigator.clipboard + .writeText(text) + .then(() => { + setCopied(true); + }) + .catch((err) => { + console.error("Failed to copy element: ", err); + window.alert(err); + }); +} + +export default function CopyComponent ({hash} : {hash: string}) { + + const [copied, setCopied] = useState(false); + const [ openCopyPopUp, setOpenCopyPopup] = useState(false); + + useEffect(() => { + if(copied === true) { + const timer = setTimeout(() => { + setCopied(false) + }, 1500); + return () => clearTimeout(timer); + } + }, [copied]); + + return ( + <> + {!copied ? ( +

{copyAddr(setCopied, hash); setOpenCopyPopup(false)}} + onMouseEnter={() => setOpenCopyPopup(true)} + onMouseLeave={() => setOpenCopyPopup(false)} + > + hash: {hash.slice(0, 7)}... +

+ ) : ( +

Hash Copied!

+ )} + + + ); +} + +const CopyPopUp = ({openCopyPopUp} : {openCopyPopUp: boolean}) => { + + // Import the current Theme + const { activeTheme } = useThemeContext(); + + return ( + <> + {openCopyPopUp && ( +
+

Click to copy Hash

+
+ )} + + ); +} \ No newline at end of file diff --git a/components/deployed-tab/deployed-tab-content.tsx b/components/deployed-tab/deployed-tab-content.tsx new file mode 100644 index 0000000..d49a0a6 --- /dev/null +++ b/components/deployed-tab/deployed-tab-content.tsx @@ -0,0 +1,54 @@ +import styles from './style.module.css'; +import { useThemeContext } from '../../context/ThemeContext' +import { TtlPopUp } from './ttl-popup'; + +type DeployedTabData = { + title: string; + displayedContracts: JSX.Element[] | string; +} + +export function DeployedTabContent(props: DeployedTabData) { + + // Import the current Theme + const { activeTheme } = useThemeContext(); + + return ( +
+ + + + + + + + + + + + + + + + +
{props.title}
ContractAddressFrom

TTL

+
+ + + + + + + + {typeof(props.displayedContracts) != "string" && ( + + {props.displayedContracts} + + )} +
+ {typeof(props.displayedContracts) == "string" && ( +
{props.displayedContracts}
+ )} +
+
+ ) +} \ No newline at end of file diff --git a/components/deployed-tab/import-contract-popups.tsx b/components/deployed-tab/import-contract-popups.tsx new file mode 100644 index 0000000..a1e6d04 --- /dev/null +++ b/components/deployed-tab/import-contract-popups.tsx @@ -0,0 +1,332 @@ +import styles from './style.module.css' +import Popup from "reactjs-popup"; +import { useState, Dispatch, SetStateAction, ChangeEvent, useEffect } from "react"; +import { useThemeContext } from "@/context/ThemeContext"; +import { useWalletContext } from "@/context/WalletContext"; +import { ImportArgsObj, importContract } from './backend'; + + +export function ImportPopup({ + importPopup, + setImportPopup +}: { + importPopup: boolean, + setImportPopup: Dispatch> +}) { + + // Import the current Theme + const { activeTheme } = useThemeContext(); + + // Import wallet infos + const walletContext = useWalletContext(); + + const [deployedName, setDeployedName] = useState(""); + const [contractId, setContractId] = useState(""); + const [admin, setAdmin] = useState(""); + const [isImporting, setIsImporting] = useState(false); + const [bumping, setBumping] = useState(null); + const [showBumpingPopup, setShowBumpingPopup] = useState(false); + const [conditionalImportButtonCss, setConditionalImportButtonCss] = useState(false); + + useEffect(() => { + if (bumping != null) { + setConditionalImportButtonCss(true); + } else { + setConditionalImportButtonCss(false); + } + }, [bumping]); + + const handleNameInputChange = (e: ChangeEvent) => { + setDeployedName(e.target.value); + } + + const handleIdInputChange = (e: ChangeEvent) => { + setContractId(e.target.value); + } + + const handleAdminInputChange = (e: ChangeEvent) => { + setAdmin(e.target.value); + } + + const importData: ImportArgsObj = { + deployed_name: deployedName, + id: contractId, + owner: admin + } + + return ( + } arrow={false}> +
+ +
Import a deployed contract to SmartDeploy
+
+
+ Contract instance name: + + +
+ Contract id: + + +
+ Contract admin: + + +
+
+

Do you want SmartDeploy to automatically bump your contract instance when it arrives at expiration time?

+
+
+ { if(bumping !== true) setBumping(true)} } + onClick={() => { if(bumping === true) setBumping(null)} } + checked={bumping === true} + /> + +
+
+ { if(bumping !== false) setBumping(false)} } + onClick={() => { if(bumping === false) setBumping(null)} } + checked={bumping === false} + /> + +
+
+
+
+
+ {(!isImporting && !bumping) ? ( + <> + + + + ) : (!isImporting && bumping) ? ( + <> + + + + {showBumpingPopup && ( + + )} + + ) : ( + + )} +
+
+
+ ); +} + +function BumpingPopup({ + showBumpingPopup, + setShowBumpingPopup, + setImportPopup, + setBumping, + setDeployedName, + setContractId, + setAdmin, + setIsImporting, + importData +}: { + showBumpingPopup: boolean + setShowBumpingPopup: Dispatch>, + setImportPopup: Dispatch>, + setDeployedName: Dispatch>, + setContractId: Dispatch>, + setAdmin: Dispatch>, + setBumping: Dispatch>, + setIsImporting: Dispatch>, + importData: ImportArgsObj +}) { + + const { activeTheme } = useThemeContext(); + const walletContext = useWalletContext(); + + enum BumpingSubscription { + SIX_MONTHS, + ONE_YEAR, + TWO_YEAR + } + + const [isDeploying, setIsDeploying] = useState(false); + const [bumpingSubscription, setBumpingSubscription] = useState(null); + const [conditionalImportButtonCss, setConditionalImportButtonCss] = useState(false); + + useEffect(() => { + if (bumpingSubscription != null) { + setConditionalImportButtonCss(true); + } else { + setConditionalImportButtonCss(false); + } + }, [bumpingSubscription]); + + + return ( +
+ + +
} arrow={false}> +
+ +
Bumping subscription for contract instance {importData.deployed_name}
+
+
+

How long do you want us to keep your contract instance alive?

+
+
+ { if(bumpingSubscription !== BumpingSubscription.SIX_MONTHS) setBumpingSubscription(BumpingSubscription.SIX_MONTHS)} } + onClick={() => { if(bumpingSubscription === BumpingSubscription.SIX_MONTHS) setBumpingSubscription(null)} } + checked={bumpingSubscription === BumpingSubscription.SIX_MONTHS} + /> + +
+
+ { if(bumpingSubscription !== BumpingSubscription.ONE_YEAR) setBumpingSubscription(BumpingSubscription.ONE_YEAR)} } + onClick={() => { if(bumpingSubscription === BumpingSubscription.ONE_YEAR) setBumpingSubscription(null)} } + checked={bumpingSubscription === BumpingSubscription.ONE_YEAR} + /> + +
+
+ { if(bumpingSubscription !== BumpingSubscription.TWO_YEAR) setBumpingSubscription(BumpingSubscription.TWO_YEAR)} } + onClick={() => { if(bumpingSubscription === BumpingSubscription.TWO_YEAR) setBumpingSubscription(null)} } + checked={bumpingSubscription === BumpingSubscription.TWO_YEAR} + /> + +
+
+
+
+
+ {!isDeploying ? ( + <> + + + + ) : ( + + )} +
+
+ + + ) +} \ No newline at end of file diff --git a/components/deployed-tab/index.tsx b/components/deployed-tab/index.tsx index 6f7360c..52b2abc 100644 --- a/components/deployed-tab/index.tsx +++ b/components/deployed-tab/index.tsx @@ -1,125 +1,53 @@ -import { FaRegClipboard } from "react-icons/fa"; -import { MdDone } from "react-icons/md" import styles from './style.module.css'; -import { smartdeploy, FetchDatas } from "@/pages"; -import { Ok, Err } from 'smartdeploy-client' -import { useState, useEffect, Dispatch, SetStateAction } from "react"; -import { useThemeContext } from '../ThemeContext' +import { DeployEventData, ClaimEventData, fetchTtlContractsData } from '@/mercury_indexer/smartdeploy-api-client'; +import { DeployedContract, listAllDeployedContracts, getMyDeployedContracts, formatCountDown, bumpAndQueryNewTtl } from './backend'; +import { useState, useEffect } from "react"; +import { DeployedTabContent } from './deployed-tab-content'; +import { ToggleButtons, Tab } from './toggle-button-component'; +import { FcOk } from "react-icons/fc"; +import { IoMdCloseCircle } from "react-icons/io"; +import CopyComponent from './copy-component'; +import { useThemeContext } from '../../context/ThemeContext' +import { useWalletContext } from '../../context/WalletContext' +import { useTimeToLiveContext, TimeToLive } from '../../context/TimeToLiveContext' +import { format } from 'date-fns' +import axios from 'axios'; +import endpoints from '@/endpoints.config'; -interface DeployedContract { - index: number; - name: string; - address: string; -} +const LEDGERS_TO_EXTEND = 535_679; -type ClipboardIconComponentProps = { - address: string; -} - - -async function listAllDeployedContracts() { - - try { - - ///@ts-ignore - const { result } = await smartdeploy.listDeployedContracts({ start: undefined, limit: undefined }); - const response = result; - - if (response instanceof Ok) { - - let deployedContracts: DeployedContract[] = []; - - const contractArray = response.unwrap(); - - ///@ts-ignore - contractArray.forEach(([name, address], i) => { - - const parsedDeployedContract: DeployedContract = { - index: i, - name: name, - address: address.toString(), - } - - deployedContracts.push(parsedDeployedContract); - - }); - - //console.log(deployedContracts); - return deployedContracts; - - } else if (response instanceof Err) { - response.unwrap(); - } else { - throw new Error("listDeployedContracts returns undefined. Impossible to fetch the deployed contracts."); - } - } catch (error) { - console.error(error); - window.alert(error); - } - -} - -async function copyAddr(setCopied: Dispatch> , addr: string) { - await navigator.clipboard - .writeText(addr) - .then(() => { - setCopied(true); - }) - .catch((err) => { - console.error("Failed to copy address: ", err); - window.alert(err); - }); -} - -function ClipboardIconComponent(props: ClipboardIconComponentProps) { - - const [copied, setCopied] = useState(false); - - useEffect(() => { - if(copied === true) { - const timer = setTimeout(() => { - setCopied(false) - }, 1500); - return () => clearTimeout(timer); - } - }, [copied]); - - return ( - <> - {!copied ? ( - - copyAddr(setCopied, props.address)} - /> - - ) : ( - -

Copied!

- - )} - - ); -} - - -export default function DeployedTab(props: FetchDatas) { +export default function DeployedTab({ + deployEvents, + claimEvents, +} : { + deployEvents: DeployEventData[] | undefined, + claimEvents: ClaimEventData[] | undefined +}) { // Import the current Theme const { activeTheme } = useThemeContext(); + // Import wallet infos + const walletContext = useWalletContext(); + // Import expiration infos + const timeToLiveMap = useTimeToLiveContext(); const [loading, setLoading] = useState(true); const [error, setError] = useState(false); - const [deployedContracts, setDeployedContracts] = useState([]); + const [selectedTab, setSelectedTab] = useState(Tab.All); + const [allDeployedContracts, setAllDeployedContracts] = useState([]); + const [myDeployedContracts, setMyDeployedContracts] = useState([]); + // useEffect to have All Deployed Contracts useEffect(() => { async function fetchDeployedContracts() { try { - const datas = await listAllDeployedContracts(); - setDeployedContracts(datas as DeployedContract[]); - setLoading(false); + const datas = await listAllDeployedContracts(deployEvents, claimEvents); + if (datas != 0) { + setAllDeployedContracts(datas as DeployedContract[]); + setLoading(false); + } } catch (error) { console.error(error); window.alert(error); @@ -127,91 +55,212 @@ export default function DeployedTab(props: FetchDatas) { } } - if (props.fetch === true) { - setLoading(true); - fetchDeployedContracts(); - props.setFetch(false); + fetchDeployedContracts(); + + }, [deployEvents, claimEvents]); + + // useEffect to have My Published Contracts + useEffect(() => { + + if (walletContext.address !== "") { + const myDeployedContracts = getMyDeployedContracts(allDeployedContracts, walletContext.address); + setMyDeployedContracts(myDeployedContracts); + } + + }, [allDeployedContracts, walletContext.address]) + + // useEffect to fetch bumping data (only when the page load) + useEffect(() => { + + async function fillAddressToTtlMap() { + + let latestLedger = (await axios.get(endpoints.rpc_endpoint)).data.history_latest_ledger; + let ttl_datas = await fetchTtlContractsData(); + + ///@ts-ignore + ttl_datas.forEach((data) => { + + // Convert TTL in a date + const timeToLiveLedger = data.live_until_ttl - latestLedger; + const timeToLiveSeconds = timeToLiveLedger * 5; + const now = new Date(); + const expirationDate = format(now.getTime() + timeToLiveSeconds * 1000, "MM/dd/yyyy"); + + if (!data.automatic_bump) { + timeToLiveMap.setAddressToTtl(prevMap => { + const updatedMap = new Map(prevMap); + updatedMap.set(data.contract_id as string, {automaticBump: false, date: expirationDate}); + return updatedMap; + }) + } else { + timeToLiveMap.setAddressToTtl(prevMap => { + const updatedMap = new Map(prevMap); + updatedMap.set(data.contract_id as string, { + automaticBump: true, + date: expirationDate, + ttlSec: timeToLiveSeconds, + countdown: formatCountDown(timeToLiveSeconds) + }); + return updatedMap; + }) + } + }); } - }, [props.fetch]); - - if (loading) return ( -
- - - - - - - - - - - - - - -
DEPLOYED CONTRACTS
ContractAddressCopy
-
- - - - - - - - - -
+ fillAddressToTtlMap(); + + }, []) + + // useEffect to count down the next bump + useEffect(() => { + + const interval = setInterval(() => { + + const newMap = new Map(timeToLiveMap.addressToTtl); + + newMap.forEach((ttl, address) => { + + if (ttl.ttlSec !== undefined) { + + // If the contract expire in less than 5 minutes (300 seconds), bump it and update the countdown + if (ttl.ttlSec > 300) { + const newValue = { + ...ttl, + ttlSec: ttl.ttlSec - 60, + countdown: formatCountDown(ttl.ttlSec - 60) + } + newMap.set(address, newValue); + } + else { + bumpAndQueryNewTtl(address, LEDGERS_TO_EXTEND, ttl, newMap); + } + + } + + }) + + timeToLiveMap.setAddressToTtl(newMap); + + }, 1000 * 60); + + return () => clearInterval(interval); + + }, [timeToLiveMap.addressToTtl]); + + if (loading) { + + return ( +
+ + {selectedTab == Tab.All ? ( + + ) : ( + walletContext.connected ? ( + + ) : ( + + ) + )}
-
- ) + ) + } - if (error) { throw new Error("Error when trying to fetch Deployed Contracts") } + else if (error) { throw new Error("Error when trying to fetch Deployed Contracts") } - if (deployedContracts) { + else if (allDeployedContracts && selectedTab == Tab.All) { - const rows: JSX.Element[] = []; + const allDeployedContractsRows: JSX.Element[] = []; - deployedContracts.forEach((deployedContract) => { - rows.push( - + allDeployedContracts.forEach((deployedContract) => { + + var version_string = undefined; + if (deployedContract.version) { + version_string = `v.${deployedContract.version.major}.${deployedContract.version.minor}.${deployedContract.version.patch}`; + } + + allDeployedContractsRows.push( + {deployedContract.name} {deployedContract.address} - + +
+ {deployedContract.version ? ( +

{deployedContract.fromPublished}

+ ) : ( + + )} +

{version_string && `(${version_string})`}

+
+ + +
+

{timeToLiveMap.addressToTtl.get(deployedContract.address)?.date}

+ {timeToLiveMap.addressToTtl.get(deployedContract.address)?.automaticBump ? ( +

bump in: {timeToLiveMap.addressToTtl.get(deployedContract.address)?.countdown}

+ ) : ( +

No automatic bump

+ )} +
+ ); }); return( -
- - - - - - - - - - - - - - -
DEPLOYED CONTRACTS
ContractAddressCopy
-
- - - - - - - - {rows} - -
-
+
+ + +
+ ) + } + + else if (myDeployedContracts && selectedTab == Tab.My) { + + const myDeployedContractsRows: JSX.Element[] = []; + + myDeployedContracts.forEach((deployedContract) => { + + var version_string = "no_evt_data"; + if (deployedContract.version) { + version_string = `v.${deployedContract.version.major}.${deployedContract.version.minor}.${deployedContract.version.patch}`; + } + + myDeployedContractsRows.push( + + {deployedContract.name} + {deployedContract.address} + +
+

{deployedContract.fromPublished}

+

({version_string})

+
+ + +
+

{timeToLiveMap.addressToTtl.get(deployedContract.address)?.date}

+ {timeToLiveMap.addressToTtl.get(deployedContract.address)?.automaticBump ? ( +

bump in: {timeToLiveMap.addressToTtl.get(deployedContract.address)?.countdown}

+ ) : ( +

No automatic bump

+ )} +
+ + + ); + }) + + return( +
+ + {walletContext.connected ? ( + myDeployedContracts.length > 0 ? ( + + ) : ( + + ) + ) : ( + + )}
) } diff --git a/components/deployed-tab/style.module.css b/components/deployed-tab/style.module.css index 12f4fe5..790ba20 100644 --- a/components/deployed-tab/style.module.css +++ b/components/deployed-tab/style.module.css @@ -1,17 +1,78 @@ .deployedTabContainer { margin-bottom: 3rem; - border-top-left-radius: 10px; - border-top-right-radius: 10px; + border-top-right-radius: 8px; box-shadow: 1.2rem 1.2rem 35px 22px var(--table-box-shadow); } +.toggleTabContainer { + display: flex; + align-items: flex-end; + justify-content: left; + width: 21rem; +} +.toggleTabContainer:hover { + cursor: pointer; +} + +.allContractsButtonSelected { + color: var(--toggle-table-button-selected-color); + background-color: var(--toggle-table-button-selected-bg); + font-size: 0.9rem; + padding: 0.6rem; + border-radius: 8px 0 0 0; + height: 2.4rem; +} + +.allContractsButtonNotSelected { + color: var(--toggle-table-button-not-selected-color); + background-color: var(--toggle-table-button-not-selected-bg); + font-size: 0.9rem; + padding: 0.6rem; + border-radius: 8px 0 0 0; +} + +.myContractsButtonSelected { + color: var(--toggle-table-button-selected-color); + background-color: var(--toggle-table-button-selected-bg); + font-size: 0.9rem; + padding: 0.6rem; + border-radius: 0 0 0 0; + height: 2.4rem; +} + +.myContractsButtonNotSelected { + color: var(--toggle-table-button-not-selected-color); + background-color: var(--toggle-table-button-not-selected-bg); + font-size: 0.9rem; + padding: 0.6rem; + border-radius: 0 0 0 0; +} + +.importContractsButton { + color: var(--toggle-table-button-not-selected-color); + background-color: var(--toggle-table-button-not-selected-bg); + font-size: 0.9rem; + padding: 0.6rem; + border-radius: 0 8px 0 0; + border-left: 2px solid var(--import-contract-button-left-border); +} +.importContractsButton:hover { + color: var(--toggle-table-button-selected-color); + background-color: var(--toggle-table-button-selected-bg); + font-size: 0.9rem; + padding: 0.6rem; + border-radius: 0 8px 0 0; + height: 2.4rem; + border-left: 0px; +} + .deployedTabHead, .deployedTabContent { table-layout: fixed; border-collapse: collapse; margin-top: 0rem; - width: 55rem; + width: 65rem; } .deployedTabContentContainer { @@ -29,17 +90,40 @@ color: var(--caption-font-color); text-align: center; padding: 0.9rem 0.4rem 0.6rem 0.4rem; - border-radius: 8px 8px 0 0; + border-radius: 0 8px 0 0; } .contractCol { - width: 24%; + width: 18%; } .addressCol { - width: 67%; + width: 55%; +} +.fromCol { + width: 12%; +} +.ttlCol { + width: 15%; } -.copyCol { - width: 9%; + +.tabMessage { + color: var(--toggle-table-button-selected-color); + padding: 0.5rem; + width: 65rem; + text-align: center; + justify-content: center; + font-size: 0.9rem; + font-weight: 500; +} + +.tabMessage { + color: var(--toggle-table-button-selected-color); + padding: 0.5rem; + width: 65rem; + text-align: center; + justify-content: center; + font-size: 0.9rem; + font-weight: 500; } .deployedTabHead thead { @@ -48,10 +132,74 @@ font-weight: bold; text-align: left; } -.copyThead { +.ttlThead { + position: relative; + display: flex; + justify-content: center; + align-items: first baseline; +} +.ttlInfoIcon { + cursor: pointer; + margin-left: 0.1rem; + transform: scale(0.6); +} +.popupContainer { + display: flex; + align-items: center; + position: absolute; + left: 63%; +} +.arrow { + transform: scale(1.5); +} +.ttlPopUpContent { + position: absolute; + left: 63%; + text-align: justify; + font-size: 0.5rem; + width: 5.5rem; + color: var(--ttl-popup-text-color); + padding: 0.3rem; + background-color: var(--ttl-popup-bg-color); + border-radius: 4px; + border: none; +} +.copyPopupContainer { + position: absolute; + left: 65%; +} +.copyPopUpContent { text-align: center; + font-size: 0.5rem; + width: 5.5rem; + color: var(--ttl-popup-text-color); + padding: 0.3rem; + background-color: var(--ttl-popup-bg-color); + border-radius: 4px; + border: none; +} +.relativeRow { + position: relative; +} +.ttlTd { + display: flex; + flex-direction: column; + align-items: center; + font-size: 0.8rem; +} +.bumpLine { + margin-top: 0.2rem; + display: flex; + align-items: center; + font-size: 0.7rem; + font-style: italic; +} +.fromTd { + position: inherit; + display: flex; + flex-direction: column; + justify-content: center; } - .deployedTabHead thead th, .deployedTabContent tbody td { padding: 0.4rem 0.5rem; @@ -71,7 +219,7 @@ } .contractCell { - text-overflow: ellipsis; + text-overflow: scroll; } .contractCell::-webkit-scrollbar { display: none; @@ -99,4 +247,161 @@ color: #8d8c8cbb; font-size: 1.5rem; font-style: italic; +} + + +.popupContainer { + background-color: var(--popup-background-color); + color: var(--popup-font-color); + font-family: Arial, Helvetica, sans-serif; + width: 40rem; + border-radius: 10px; + border: solid 1px rgb(76, 75, 75); + flex-direction: column; +} +.bumpingPopupContainer { + background-color: var(--popup-background-color); + color: var(--popup-font-color); + font-family: Arial, Helvetica, sans-serif; + width: 40rem; + border-radius: 10px; + border: solid 1px rgb(76, 75, 75); +} +.header { + width: 100%; + border-bottom: 1px solid gray; + font-size: 1.9rem; + font-weight: bold; + text-align: center; + padding: 0.7rem; +} +.nameColor { + color: var(--popup-important-font-color); +} +.content { + width: 100%; + text-align: center; + font-size: 1.2rem; + margin-top: 1rem; + margin-bottom: 1rem; + padding-left: 0.7rem; + padding-right: 0.7rem; +} + +.inputsDiv { + display: flex; + flex-direction: column; + align-items: center; + margin-top: 0.5rem; + margin-bottom: 2rem; +} +.input { + outline: none; + font-size: 1.3rem; + font-weight: bold; + margin-top: 0.6rem; + width: 45%; + background-color: transparent; + padding: 0.5rem 0.4rem; + border: 1px solid gray; + border-radius: 5px; + color: var(--popup-input-font-color); +} +.deployedNameInput::placeholder { + color: rgba(128, 128, 128, 0.51); +} + +.bumpContainer { + text-align: justify; + padding-left: 2.2rem; + padding-right: 2.2rem; +} + +.bumpContainer > p { + font-size: 1rem; + font-weight: 700; + color: var(--popup-input-font-color); +} + +.checkboxContainer { + display: flex; + flex-direction: column; + margin-top: 0.6rem; + margin-left: 0.9rem; +} +.checkboxContainer > div { + display: flex; + margin-top: 0.4rem; +} +.checkboxContainer > div > input[type="checkbox"] { + transform: scale(1.5); + margin-right: 0.5rem; + accent-color: rgb(18, 194, 18); +} +.checkboxContainer > div > label { + font-size: 1rem; + color: var(--popup-input-font-color); +} + +.buttonContainer { + width: 100%; + padding-top: 0.5rem; + padding-bottom: 1rem; + text-align: center; +} + +.button { + margin-right: 2rem; + margin-left: 2rem; + padding: 0.2rem 2.5rem 0.2rem 2.5rem; + width: 15rem; + font-size: 1.2rem; + font-weight: bold; + background-color: var(--popup-button-background-color); + color: var(--popup-button-font-color); + border: 1.5px solid var(--popup-button-font-color); + border-radius: 5px; +} +.button:hover { + background-color: var(--popup-button-hover); +} +.buttonDisabled { + margin-right: 2rem; + margin-left: 2rem; + padding: 0.2rem 2.5rem 0.2rem 2.5rem; + width: 15rem; + font-size: 1.2rem; + font-weight: bold; + background-color: var(--popup-button-disabled-background-color); + color: var(--popup-button-disabled-font-color); + border: 1.5px solid var(--popup-button-disabled-font-color); + border-radius: 5px; +} +.buttonWhenDeploying { + margin-right: 2rem; + margin-left: 2rem; + padding: 0.2rem 2.5rem 0.2rem 2.5rem; + width: 15rem; + font-size: 1.2rem; + font-weight: bold; + background-color: var(--popup-button-background-color); + color: var(--popup-button-font-color); + border: 1.5px solid var(--popup-button-font-color); + border-radius: 5px; +} + +.close { + cursor: pointer; + outline: none; + color: red; + position: absolute; + display: block; + padding: 2px 5px; + line-height: 20px; + right: -10px; + top: -10px; + font-size: 24px; + background: #ffffff; + border-radius: 18px; + border: 1px solid #cfcece; } \ No newline at end of file diff --git a/components/deployed-tab/toggle-button-component.tsx b/components/deployed-tab/toggle-button-component.tsx new file mode 100644 index 0000000..ff4be5e --- /dev/null +++ b/components/deployed-tab/toggle-button-component.tsx @@ -0,0 +1,60 @@ +import styles from './style.module.css' +import { useThemeContext } from '../../context/ThemeContext' +import { useWalletContext } from '@/context/WalletContext'; +import { Dispatch, SetStateAction } from "react"; +import { useState } from 'react'; +import { ImportPopup } from './import-contract-popups'; + +export enum Tab { + All, + My, +} + +export function ToggleButtons({ + selectedTab, + setSelectedTab +}: { + selectedTab: Tab, + setSelectedTab: Dispatch> +}) { + + // Import the current Theme + const { activeTheme } = useThemeContext(); + + // Import wallet infos + const walletContext = useWalletContext(); + + const [importPopup, setImportPopup] = useState(false); + + return ( + <> +
+
{ setSelectedTab(Tab.All) }} + > + All Contracts +
+
{ setSelectedTab(Tab.My) }} + > + My Contracts +
+
{ + if (walletContext.connected) { + setImportPopup(true); + } else { + window.alert("Please connect your wallet to import a contract"); + } + }} + > + Import Contract +
+
+ + + ) +} \ No newline at end of file diff --git a/components/deployed-tab/ttl-popup.tsx b/components/deployed-tab/ttl-popup.tsx new file mode 100644 index 0000000..a749ad5 --- /dev/null +++ b/components/deployed-tab/ttl-popup.tsx @@ -0,0 +1,31 @@ +import styles from './style.module.css'; +import { BiCaretLeft } from "react-icons/bi"; +import { useState } from 'react'; +import { IoMdInformationCircleOutline } from "react-icons/io"; +import { useThemeContext } from '../../context/ThemeContext' + +export const TtlPopUp = () => { + + // Import the current Theme + const { activeTheme } = useThemeContext(); + const [ openTtlPopUp, setOpenTtlPopup] = useState(false); + + return ( + <> + setOpenTtlPopup(true)} + onMouseLeave={() => setOpenTtlPopup(false)} + > + + + { openTtlPopUp && ( +
+ +
+

Time To Live of the contract instance

+
+
+ )} + + ); +} diff --git a/components/published-tab/backend.ts b/components/published-tab/backend.ts new file mode 100644 index 0000000..5d8304e --- /dev/null +++ b/components/published-tab/backend.ts @@ -0,0 +1,232 @@ +import { Dispatch, SetStateAction } from 'react' +import { isConnected } from '@stellar/freighter-api' +import { smartdeploy } from "@/pages" +import { PublishEventData, DeployEventData, readTtl, addDbTtlData } from '@/mercury_indexer/smartdeploy-api-client' +import { Ok, Err, Option, Version } from 'smartdeploy-client' +import { WalletContextType } from '@/context/WalletContext' +import { TimeToLiveType } from '@/context/TimeToLiveContext' +import { format } from 'date-fns' +import { formatCountDown } from '../deployed-tab/backend' + +export interface PublishedContract { + index: number; + name: string; + author: string; + versions: { + version: Version, + nb_instances: number, + hash: string + }[]; +} + +export async function listAllPublishedContracts( + deploy_events: DeployEventData[] | undefined, +) { + + if (deploy_events) { + try { + + ///@ts-ignore + const {result} = await smartdeploy.listPublishedContracts({ start: undefined, limit: undefined }); + const response = result; + + if (response instanceof Ok) { + let publishedContracts: PublishedContract[] = []; + + const contractArray = response.unwrap(); + + ///@ts-ignore + contractArray.forEach(([name, publishedContract], i) => { + + let versions: {version: Version, hash: string, nb_instances: number}[] = []; + + ///@ts-ignore + Array.from(publishedContract.versions).forEach((contractDatas: [Version, any]) => { + + // Version object + const version = contractDatas[0]; + + // Nb instances per version + const nb_instances = deploy_events.filter(event => (event.publishedName == name && areVersionsEqual(event.version, version))).length ?? 0; + + // hash + const hash = contractDatas[1].hash.join(''); + + versions.push({version, hash, nb_instances}); + }); + + const parsedPublishedContract: PublishedContract = { + index: i, + name: name, + author: publishedContract.author.toString(), + versions: versions + }; + + publishedContracts.push(parsedPublishedContract); + }); + + return publishedContracts; + + } else if (response instanceof Err) { + response.unwrap(); + } else { + throw new Error("listPublishedContracts returns undefined. Impossible to fetch the published contracts."); + } + } catch (error) { + console.error(error); + window.alert(error); + } + } + else { + return 0; + } + +} + +export function getMyPublishedContracts( + publishedContracts: PublishedContract[], + deploy_events: DeployEventData[] | undefined, + address: string +) { + + var myPublishedContracts: PublishedContract[] = []; + + const contractsIOwn = publishedContracts.filter(contract => contract.author === address); + + const nameIDeployed = deploy_events!.filter(event => event.deployer == address).map(event => event.publishedName); + const contractsIDeployed = publishedContracts.filter(contract => nameIDeployed.includes(contract.name)); + + myPublishedContracts.push(...contractsIOwn, ...contractsIDeployed); + + const ret = myPublishedContracts.filter((contract, index) => { + return myPublishedContracts.findIndex(item => item.name === contract.name) === index; + }); + + return ret; + +} + +function areVersionsEqual(v1: Version, v2: Version) { + return v1.major == v2.major && v1.minor == v2.minor && v1.patch == v2.patch; +} + +export type DeployArgsObj = { + contract_name: string, + version: Option, + deployed_name: string, + owner: string, + salt: Option, + init: Option]> +}; + +export async function deploy( + walletContext: WalletContextType, + argsObj: DeployArgsObj +) { + + // Check if the user has Freighter + if (!(await isConnected())) { + window.alert("Impossible to interact with Soroban: you don't have Freighter extension.\n You can install the extension here: https://www.freighter.app/"); + } + else { + // Check if the Wallet is connected + if (walletContext.address === "") { + alert("Wallet not connected. Please, connect a Stellar account."); + } + // Check is the network is Futurenet + else if (walletContext.network.replace(" ", "").toUpperCase() !== "TESTNET") { + alert("Wrong Network. Please, switch to Testnet."); + } + else { + // Check if deployed name is empty + if (argsObj.deployed_name === "") { + alert("Deployed name cannot be empty. Please, choose a deployed name."); + } + // Check if deployed name contains spaces + else if (argsObj.deployed_name.includes(' ')) { + alert("Deployed name cannot includes spaces. Please, remove the spaces."); + } + // Now that everything is ok, deploy the contract + else { + + try { + + const tx = await smartdeploy.deploy(argsObj); + // Sign and send the transaction + const signedTx = await tx.signAndSend(); + + // Retrieve the id of the deployed contract + const contract_id = signedTx.result.unwrap(); + return contract_id; + + } catch (error) { + console.error(error); + window.alert(error); + return false; + } + + } + } + } +} + +export async function deployContract( + walletContext: WalletContextType, + deployData: DeployArgsObj, + timeToLiveMap: TimeToLiveType, + setIsDeploying: Dispatch>, + setDeployedName: Dispatch>, + setBumping: Dispatch>, + setWouldDeploy: Dispatch>, + subscribeBumping: boolean +) { + + setIsDeploying(true); + + let id = await deploy( + walletContext, + deployData + ); + + if (typeof id === "string") { + + const ttlData = await readTtl(id); + + const latestLedger = ttlData[0]; + const liveUntil = ttlData[1]; + const timeToLiveLedger = liveUntil - latestLedger; + + // Convert TTL in a date + const timeToLiveSeconds = timeToLiveLedger * 5; + const now = new Date(); + const expirationDate = format(now.getTime() + timeToLiveSeconds * 1000, "MM/dd/yyyy"); + + if (!subscribeBumping) { + timeToLiveMap.setAddressToTtl(prevMap => { + const updatedMap = new Map(prevMap); + updatedMap.set(id as string, {automaticBump: false, date: expirationDate}); + return updatedMap; + }) + } else { + timeToLiveMap.setAddressToTtl(prevMap => { + const updatedMap = new Map(prevMap); + updatedMap.set(id as string, { + automaticBump: true, + date: expirationDate, + ttlSec: timeToLiveSeconds, + countdown: formatCountDown(timeToLiveSeconds) + }); + return updatedMap; + }) + } + + await addDbTtlData(id, subscribeBumping, liveUntil); + + setIsDeploying(false) + setDeployedName("") + setBumping(null) + setWouldDeploy(false) + } else { + setIsDeploying(false) + } +} \ No newline at end of file diff --git a/components/published-tab/deploy-components.tsx b/components/published-tab/deploy-components.tsx new file mode 100644 index 0000000..c5496df --- /dev/null +++ b/components/published-tab/deploy-components.tsx @@ -0,0 +1,386 @@ +import styles from './style.module.css' + +import { useThemeContext } from '../../context/ThemeContext' +import { useWalletContext } from '../../context/WalletContext' +import { useTimeToLiveContext } from '../../context/TimeToLiveContext' +import { useState, ChangeEvent, Dispatch, SetStateAction, useEffect } from 'react' +import Popup from 'reactjs-popup' +import Dropdown from 'react-dropdown' +import { BsSendPlus } from 'react-icons/bs' +import { IoMdArrowDropdown, IoMdArrowDropup } from 'react-icons/io' + +import { DeployArgsObj, deployContract } from './backend' +import { Version } from 'smartdeploy-client' + +type DeployVersionProps = { + contract_name: string; + selected_version: {version: Version, version_string: string}; +} + +function DeployIconComponent(props: DeployVersionProps) { + + // Import the current Theme + const { activeTheme } = useThemeContext(); + + const walletContext = useWalletContext(); + + // Context to store Data Expiration Ledger + const timeToLiveMap = useTimeToLiveContext(); + + const [wouldDeploy, setWouldDeploy] = useState(false); + const [deployedName, setDeployedName] = useState(""); + const [isDeploying, setIsDeploying] = useState(false); + const [bumping, setBumping] = useState(null); + const [showBumpingPopup, setShowBumpingPopup] = useState(false); + const [conditionalDeployButtonCss, setConditionalDeployButtonCss] = useState(false); + + useEffect(() => { + if (bumping != null) { + setConditionalDeployButtonCss(true); + } else { + setConditionalDeployButtonCss(false); + } + }, [bumping]); + + const handleInputChange = (e: ChangeEvent) => { + setDeployedName(e.target.value); + } + + const deployData: DeployArgsObj = { + contract_name: props.contract_name, + version: props.selected_version.version, + deployed_name: deployedName, + owner: walletContext.address, + salt: undefined, + init: undefined + } + + return ( + <> + {!wouldDeploy ? ( + + setWouldDeploy(true) } + /> + + ) : ( + <> + +

Deploying...

+ + +
+ +
Deploy {props.contract_name} ({props.selected_version.version_string})
+
+

You are about to create an instance of {props.contract_name} ({props.selected_version.version_string}) where you will be the owner.

+
+ Please choose a contract instance name: + + +
+
+

Do you want SmartDeploy to automatically bump your contract instance when it arrives at expiration time?

+
+
+ { if(bumping !== true) setBumping(true)} } + onClick={() => { if(bumping === true) setBumping(null)} } + checked={bumping === true} + /> + +
+
+ { if(bumping !== false) setBumping(false)} } + onClick={() => { if(bumping === false) setBumping(null)} } + checked={bumping === false} + /> + +
+
+
+
+
+ {(!isDeploying && !bumping) ? ( + <> + + + + ) : (!isDeploying && bumping) ? ( + <> + + + + {showBumpingPopup && ( + + )} + + ) : ( + + )} +
+
+
+ + )} + + ); +} + +function BumpingPopup({ + showBumpingPopup, + setShowBumpingPopup, + setBumping, + setDeployedName, + setWouldDeploy, + deployData +}: { + showBumpingPopup: boolean + setShowBumpingPopup: Dispatch>, + setDeployedName: Dispatch>, + setBumping: Dispatch>, + setWouldDeploy: Dispatch>, + deployData: DeployArgsObj +}) { + + const { activeTheme } = useThemeContext(); + const walletContext = useWalletContext(); + + // Context to store Data Expiration Ledger + const timeToLiveMap = useTimeToLiveContext(); + + enum BumpingSubscription { + SIX_MONTHS, + ONE_YEAR, + TWO_YEAR + } + + const [isDeploying, setIsDeploying] = useState(false); + const [bumpingSubscription, setBumpingSubscription] = useState(null); + const [conditionalDeployButtonCss, setConditionalDeployButtonCss] = useState(false); + + useEffect(() => { + if (bumpingSubscription != null) { + setConditionalDeployButtonCss(true); + } else { + setConditionalDeployButtonCss(false); + } + }, [bumpingSubscription]); + + return ( +
+ + +
} arrow={false}> +
+ +
Bumping subscription for contract instance {deployData.deployed_name}
+
+
+

How long do you want us to keep your contract instance alive?

+
+
+ { if(bumpingSubscription !== BumpingSubscription.SIX_MONTHS) setBumpingSubscription(BumpingSubscription.SIX_MONTHS)} } + onClick={() => { if(bumpingSubscription === BumpingSubscription.SIX_MONTHS) setBumpingSubscription(null)} } + checked={bumpingSubscription === BumpingSubscription.SIX_MONTHS} + /> + +
+
+ { if(bumpingSubscription !== BumpingSubscription.ONE_YEAR) setBumpingSubscription(BumpingSubscription.ONE_YEAR)} } + onClick={() => { if(bumpingSubscription === BumpingSubscription.ONE_YEAR) setBumpingSubscription(null)} } + checked={bumpingSubscription === BumpingSubscription.ONE_YEAR} + /> + +
+
+ { if(bumpingSubscription !== BumpingSubscription.TWO_YEAR) setBumpingSubscription(BumpingSubscription.TWO_YEAR)} } + onClick={() => { if(bumpingSubscription === BumpingSubscription.TWO_YEAR) setBumpingSubscription(null)} } + checked={bumpingSubscription === BumpingSubscription.TWO_YEAR} + /> + +
+
+
+
+
+ {!isDeploying ? ( + <> + + + + ) : ( + + )} +
+
+ +
+ ) +} + +type DeployProps = { + contract_name: string; + contract_author: string; + versions: {version: Version, version_string: string, nb_instances: number}[]; +} + +export function DeployVersionComponent(props: DeployProps) { + + // The default selected version is the last one + const defaultSelectedVersion = { + version: props.versions[0].version, + version_string: props.versions[0].version_string + }; + + const [selectedVersion, setSelectedVersion] = useState<{version: Version, version_string: string}>(defaultSelectedVersion); + + // Number of deployed instances per version + const inst = props.versions.find(v => v.version_string === selectedVersion.version_string)?.nb_instances ?? 0; + + return ( + <> + + {inst} + + {props.contract_author} + + + + + + ) +} + +type VersionDropdownProps = { + versions: {version: Version, version_string: string}[]; + selected_version: {version: Version, version_string: string}; + set_selected_version: Dispatch>; +} + +export function VersionDropdownButton(props: VersionDropdownProps) { + + const versions: string[] = []; + + props.versions.forEach((version) => { + versions.push(version.version_string); + }); + + return( +
+ } + arrowOpen={} + onChange={(version) => { + + const newSelectedVersionString = version.value; + + const [major, minor, patch] = version.value.split('.').slice(1); + const newSelectedVersion: Version = { + major: parseInt(major), + minor: parseInt(minor), + patch: parseInt(patch), + } + + props.set_selected_version({version: newSelectedVersion, version_string: newSelectedVersionString}); + }} + /> +
+ ) +} \ No newline at end of file diff --git a/components/published-tab/index.tsx b/components/published-tab/index.tsx index 72b51ac..e39105d 100644 --- a/components/published-tab/index.tsx +++ b/components/published-tab/index.tsx @@ -1,427 +1,163 @@ -import { BsSendPlus } from 'react-icons/bs'; -import { IoMdArrowDropdown, IoMdArrowDropup } from 'react-icons/io' -import Popup from 'reactjs-popup'; import styles from './style.module.css'; -import Dropdown from 'react-dropdown'; -import { smartdeploy, StateVariablesProps, UserWalletInfo, FetchDatas } from "@/pages"; -import { isConnected } from '@stellar/freighter-api'; -import { Ok, Err, Option, Version } from 'smartdeploy-client' -import { useState, useEffect, ChangeEvent, Dispatch, SetStateAction } from 'react'; -import { useThemeContext } from '../ThemeContext' +import { useThemeContext } from '../../context/ThemeContext' +import { useState, useEffect } from 'react'; +import { PublishTabContent } from './publish-tab-content'; +import { ToggleButtons, Tab } from './toggle-button-component'; +import { Version } from 'smartdeploy-client' +import { PublishEventData, DeployEventData } from '@/mercury_indexer/smartdeploy-api-client'; +import { PublishedContract, listAllPublishedContracts, getMyPublishedContracts } from './backend' +import { DeployVersionComponent } from './deploy-components' +import { useWalletContext } from '../../context/WalletContext' + +export default function PublishedTab({ + publishEvents, + deployEvents +} : { + publishEvents: PublishEventData[] | undefined, + deployEvents: DeployEventData[] | undefined +}) { -interface PublishedContract { - index: number; - name: string; - author: string; - versions: { - version: Version, - hash: string - }[]; -} - -type DeployProps = { - userWalletInfo: UserWalletInfo; - refetchDeployedContract: FetchDatas; - contract_name: string; - versions: {version: Version, version_string: string}[]; -} - -type DeployVersionProps = { - userWalletInfo: UserWalletInfo; - refetchDeployedContract: FetchDatas; - contract_name: string; - selected_version: {version: Version, version_string: string}; -} - -type VersionDropdownProps = { - versions: {version: Version, version_string: string}[]; - selected_version: {version: Version, version_string: string}; - set_selected_version: Dispatch>; -} - -type DeployArgsObj = { - contract_name: string, - version: Option, - deployed_name: string, - owner: string, - salt: Option -}; - -async function listAllPublishedContracts() { - - try { - - ///@ts-ignore - const {result} = await smartdeploy.listPublishedContracts({ start: undefined, limit: undefined }); - const response = result; - - if (response instanceof Ok) { - let publishedContracts: PublishedContract[] = []; - - const contractArray = response.unwrap(); - - ///@ts-ignore - contractArray.forEach(([name, publishedContract], i) => { - - let versions: {version: Version, hash: string}[] = []; - - ///@ts-ignore - Array.from(publishedContract.versions).forEach((contractDatas: [Version, any]) => { - - // Version object - const version = contractDatas[0]; - - // hash - const hash = contractDatas[1].hash.join(''); - - versions.push({version, hash}); - }); - - const parsedPublishedContract: PublishedContract = { - index: i, - name: name, - author: publishedContract.author.toString(), - versions: versions - }; - - publishedContracts.push(parsedPublishedContract); - }); - - return publishedContracts; + // Import the current Theme + const { activeTheme } = useThemeContext(); + // Import wallet infos + const walletContext = useWalletContext(); - } else if (response instanceof Err) { - response.unwrap(); - } else { - throw new Error("listPublishedContracts returns undefined. Impossible to fetch the published contracts."); - } - } catch (error) { - console.error(error); - window.alert(error); - } + const [loading, setLoading] = useState(true); + const [error, setError] = useState(false); + const [selectedTab, setSelectedTab] = useState(Tab.All); + const [allPublishedContracts, setAllPublishedContracts] = useState([]); + const [myPublishedContracts, setMyPublishedContracts] = useState([]); -} + // useEffect to have All Published Binaries + useEffect(() => { -async function deploy( - userWalletInfo: UserWalletInfo, - refetchDeployedContract: FetchDatas, - setIsDeploying: Dispatch>, - setDeployedName: Dispatch>, - setWouldDeploy: Dispatch>, - argsObj: DeployArgsObj -) { - - // Check if the user has Freighter - if (!(await isConnected())) { - window.alert("Impossible to interact with Soroban: you don't have Freighter extension.\n You can install the extension here: https://www.freighter.app/"); - setIsDeploying(false); - } - else { - // Check if the Wallet is connected - if (userWalletInfo.address === "") { - alert("Wallet not connected. Please, connect a Stellar account."); - setIsDeploying(false); - } - // Check is the network is Futurenet - else if (userWalletInfo.network.replace(" ", "").toUpperCase() !== "TESTNET") { - alert("Wrong Network. Please, switch to Testnet."); - setIsDeploying(false); - } - else { - // Check if deployed name is empty - if (argsObj.deployed_name === "") { - alert("Deployed name cannot be empty. Please, choose a deployed name."); - setIsDeploying(false); - } - // Check if deployed name contains spaces - else if (argsObj.deployed_name.includes(' ')) { - alert("Deployed name cannot includes spaces. Please, remove the spaces."); - setIsDeploying(false); + async function fetchAllPublishedContracts() { + try { + const datas = await listAllPublishedContracts(deployEvents); + if (datas != 0) { + setAllPublishedContracts(datas as PublishedContract[]); + setLoading(false); + } + } catch (error) { + console.error(error); + window.alert(error); + setError(true); } - // Now that everything is ok, deploy the contract - else { - - try { + } - const tx = await smartdeploy.deploy(argsObj, { responseType: 'full' }); - console.log(tx); - refetchDeployedContract.setFetch(true); - setDeployedName(""); - setWouldDeploy(false); - + fetchAllPublishedContracts(); - } catch (error) { - console.error(error); - window.alert(error); - } + }, [publishEvents, deployEvents]) - setIsDeploying(false); + // useEffect to have My Published Binaries + useEffect(() => { - } + if ((walletContext.address !== "") && deployEvents) { + const myPublishedContracts = getMyPublishedContracts(allPublishedContracts, deployEvents, walletContext.address); + setMyPublishedContracts(myPublishedContracts); } - } -} - -function VersionDropdownButton(props: VersionDropdownProps) { - - const versions: string[] = []; - - props.versions.forEach((version) => { - versions.push(version.version_string); - }); - return( -
- } - arrowOpen={} - onChange={(version) => { + }, [allPublishedContracts, walletContext.address]) - const newSelectedVersionString = version.value; - - const [major, minor, patch] = version.value.split('.').slice(1); - const newSelectedVersion: Version = { - major: parseInt(major), - minor: parseInt(minor), - patch: parseInt(patch), - } - - props.set_selected_version({version: newSelectedVersion, version_string: newSelectedVersionString}); - }} - /> -
- ) -} - -function DeployIconComponent(props: DeployVersionProps) { - - // Import the current Theme - const { activeTheme } = useThemeContext(); - - const [wouldDeploy, setWouldDeploy] = useState(false); - const [deployedName, setDeployedName] = useState(""); - const [isDeploying, setIsDeploying] = useState(false); - - const handleInputChange = (e: ChangeEvent) => { - setDeployedName(e.target.value); + if (loading) { + + return ( +
+ + {selectedTab == Tab.All ? ( + + ) : ( + walletContext.connected ? ( + + ) : ( + + ) + )} +
+ ); } - - return ( - <> - {!wouldDeploy ? ( - - setWouldDeploy(true) } - /> - - ) : ( - <> - -

Deploying...

- - -
- -
Deploy {props.contract_name} ({props.selected_version.version_string})
-
-

You are about to create an instance of {props.contract_name} published contract where you will be the owner.

-
- Please choose a contract instance name: - - -
-
-
- {!isDeploying ? ( - <> - - - - ) : ( - - )} -
-
-
- - )} - - ); -} - -function DeployVersionComponent(props: DeployProps) { - // The default selected version is the last one - const defaultSelectedVersion = { - version: props.versions[0].version, - version_string: props.versions[0].version_string - }; + else if (error) { throw new Error("Error when trying to fetch Published Binaries");} - const [selectedVersion, setSelectedVersion] = useState<{version: Version, version_string: string}>(defaultSelectedVersion); + else if (allPublishedContracts && selectedTab == Tab.All) { - return ( - <> - - - - - - ) -} + const allPublishedContractsRows: JSX.Element[] = []; + allPublishedContracts.forEach((publishedContract) => { -export default function PublishedTab(props: StateVariablesProps) { + const versions: {version: Version, version_string: string, nb_instances: number}[] = []; - // Import the current Theme - const { activeTheme } = useThemeContext(); - - const [loading, setLoading] = useState(true); - const [error, setError] = useState(false); - const [publishedContracts, setPublishedContracts] = useState([]); + publishedContract.versions.forEach((obj) => { - useEffect(() => { + // Version obj + const version = obj.version; - async function fetchPublishedContracts() { - try { - const datas = await listAllPublishedContracts(); - setPublishedContracts(datas as PublishedContract[]); - setLoading(false); - } catch (error) { - console.error(error); - window.alert(error); - setError(true); - } - } + // Nb of instances for that version + const nb_instances = obj.nb_instances; - if (props.fetchPublished?.fetch === true) { - setLoading(true); - fetchPublishedContracts(); - props.fetchPublished.setFetch(false); - } + // Version string + const major = version.major; + const minor = version.minor; + const patch = version.patch; + const version_string = `v.${major}.${minor}.${patch}`; - }, [props.fetchPublished?.fetch]); - - if (loading) return ( -
- - - - - - - - - - - - - - - - -
PUBLISHED CONTRACTS
ContractAuthorVersionDeploy
-
- - - - - - - - - - -
+ versions.push({version, version_string, nb_instances}); + }) + versions.reverse(); + + allPublishedContractsRows.push( + + {publishedContract.name} + + + ); + }); + + return( +
+ +
-
- ); - - else if (error) { throw new Error("Error when trying to fetch Published Contracts");} + ) + } - else if (publishedContracts) { + else if (myPublishedContracts && selectedTab == Tab.My) { - const rows: JSX.Element[] = []; + const myPublishedContractsRows: JSX.Element[] = []; - publishedContracts.forEach((publishedContract) => { + myPublishedContracts.forEach((publishedContract) => { - const versions: {version: Version, version_string: string}[] = []; + const versions: {version: Version, version_string: string, nb_instances: number}[] = []; publishedContract.versions.forEach((obj) => { // Version obj const version = obj.version; + // Nb of instances for that version + const nb_instances = obj.nb_instances; + // Version string const major = version.major; const minor = version.minor; const patch = version.patch; const version_string = `v.${major}.${minor}.${patch}`; - versions.push({version, version_string}); + versions.push({version, version_string, nb_instances}); }) versions.reverse(); - rows.push( + myPublishedContractsRows.push( {publishedContract.name} - {publishedContract.author} @@ -429,37 +165,17 @@ export default function PublishedTab(props: StateVariablesProps) { }); return( -
- - - - - - - - - - - - - - - - -
PUBLISHED CONTRACTS
ContractAuthorVersionsDeploy
-
- - - - - - - - - {rows} - -
-
+
+ + {walletContext.connected ? ( + myPublishedContracts.length > 0 ? ( + + ) : ( + + ) + ) : ( + + )}
) } diff --git a/components/published-tab/publish-tab-content.tsx b/components/published-tab/publish-tab-content.tsx new file mode 100644 index 0000000..61ae841 --- /dev/null +++ b/components/published-tab/publish-tab-content.tsx @@ -0,0 +1,56 @@ +import styles from './style.module.css'; +import { useThemeContext } from '../../context/ThemeContext' + +type PublishTabData = { + title: string; + displayedContracts: JSX.Element[] | string; +} + +export function PublishTabContent(props: PublishTabData) { + + // Import the current Theme + const { activeTheme } = useThemeContext(); + + return ( +
+ + + + + + + + + + + + + + + + + + +
{props.title}
Binary nameInstancesAuthorVersionsDeploy
+
+ + + + + + + + + {typeof(props.displayedContracts) != "string" && ( + + {props.displayedContracts} + + )} +
+ {typeof(props.displayedContracts) == "string" && ( +
{props.displayedContracts}
+ )} +
+
+ ) +} \ No newline at end of file diff --git a/components/published-tab/style.module.css b/components/published-tab/style.module.css index 6abf9de..18686d2 100644 --- a/components/published-tab/style.module.css +++ b/components/published-tab/style.module.css @@ -1,17 +1,60 @@ .publishedTabContainer { margin-bottom: 5rem; - border-top-left-radius: 10px; - border-top-right-radius: 10px; + border-top-right-radius: 8px; box-shadow: 1.2rem 1.2rem 35px 22px var(--table-box-shadow); } +.toggleTabContainer { + display: flex; + align-items: flex-end; + justify-content: left; + width: 14rem; +} +.toggleTabContainer:hover { + cursor: pointer; +} + +.allContractsButtonSelected { + color: var(--toggle-table-button-selected-color); + background-color: var(--toggle-table-button-selected-bg); + font-size: 0.9rem; + padding: 0.6rem; + border-radius: 8px 0 0 0; + height: 2.4rem; +} + +.allContractsButtonNotSelected { + color: var(--toggle-table-button-not-selected-color); + background-color: var(--toggle-table-button-not-selected-bg); + font-size: 0.9rem; + padding: 0.6rem; + border-radius: 8px 0 0 0; +} + +.myContractsButtonSelected { + color: var(--toggle-table-button-selected-color); + background-color: var(--toggle-table-button-selected-bg); + font-size: 0.9rem; + padding: 0.6rem; + border-radius: 0 8px 0 0; + height: 2.4rem; +} + +.myContractsButtonNotSelected { + color: var(--toggle-table-button-not-selected-color); + background-color: var(--toggle-table-button-not-selected-bg); + font-size: 0.9rem; + padding: 0.6rem; + border-radius: 0 8px 0 0; +} + .publishedTabHead, .publishedTabContent { table-layout: fixed; border-collapse: collapse; margin-top: 0rem; - width: 55rem; + width: 65rem; } .publishedTabContentContainer { @@ -29,22 +72,42 @@ color: var(--caption-font-color); text-align: center; padding: 0.9rem 0.4rem 0.6rem 0.4rem; - border-radius: 8px 8px 0 0; + border-radius: 0 8px 0 0; } .contractCol { - width: 18%; + width: 14%; +} +.instancesCol { + width: 9%; } .authorCol { - width: 64%; + width: 57%; } .versionCol { - width: 10%; + width: 12%; } .deployCol { width: 8%; } +.instancesThead { + text-align: center; +} +.instancesTd { + text-align: center; +} + +.tabMessage { + color: var(--toggle-table-button-selected-color); + padding: 0.5rem; + width: 65rem; + text-align: center; + justify-content: center; + font-size: 0.9rem; + font-weight: 500; +} + .publishedTabHead thead { background-color: var(--thead-background-color); color: var(--thead-font-color); @@ -73,7 +136,7 @@ } .contractCell { - text-overflow: ellipsis; + text-overflow: scroll; } .contractCell::-webkit-scrollbar { display: none; @@ -113,6 +176,14 @@ border-radius: 10px; border: solid 1px rgb(76, 75, 75); } +.bumpingPopupContainer { + background-color: var(--popup-background-color); + color: var(--popup-font-color); + font-family: Arial, Helvetica, sans-serif; + width: 40rem; + border-radius: 10px; + border: solid 1px rgb(76, 75, 75); +} .header { width: 100%; border-bottom: 1px solid gray; @@ -155,14 +226,45 @@ } .deployedNameInput::placeholder { color: rgba(128, 128, 128, 0.51); - } +} + +.bumpContainer { + text-align: justify; + padding-left: 2.2rem; + padding-right: 2.2rem; +} + +.bumpContainer > p { + font-size: 1rem; + font-weight: 700; + color: var(--popup-input-font-color); +} + +.checkboxContainer { + display: flex; + flex-direction: column; + margin-top: 0.6rem; + margin-left: 0.9rem; +} +.checkboxContainer > div { + display: flex; + margin-top: 0.4rem; +} +.checkboxContainer > div > input[type="checkbox"] { + transform: scale(1.5); + margin-right: 0.5rem; + accent-color: rgb(18, 194, 18); +} +.checkboxContainer > div > label { + font-size: 1rem; + color: var(--popup-input-font-color); +} .buttonContainer { width: 100%; padding-top: 0.5rem; padding-bottom: 1rem; text-align: center; - } .button { @@ -180,6 +282,18 @@ .button:hover { background-color: var(--popup-button-hover); } +.buttonDisabled { + margin-right: 2rem; + margin-left: 2rem; + padding: 0.2rem 2.5rem 0.2rem 2.5rem; + width: 15rem; + font-size: 1.2rem; + font-weight: bold; + background-color: var(--popup-button-disabled-background-color); + color: var(--popup-button-disabled-font-color); + border: 1.5px solid var(--popup-button-disabled-font-color); + border-radius: 5px; +} .buttonWhenDeploying { margin-right: 2rem; margin-left: 2rem; diff --git a/components/published-tab/toggle-button-component.tsx b/components/published-tab/toggle-button-component.tsx new file mode 100644 index 0000000..0f44350 --- /dev/null +++ b/components/published-tab/toggle-button-component.tsx @@ -0,0 +1,37 @@ +import styles from './style.module.css' +import { useThemeContext } from '../../context/ThemeContext' +import { Dispatch, SetStateAction } from "react"; + +export enum Tab { + All, + My +} + +export function ToggleButtons({ + selectedTab, + setSelectedTab +}: { + selectedTab: Tab, + setSelectedTab: Dispatch> +}) { + + // Import the current Theme + const { activeTheme } = useThemeContext(); + + return ( +
+
{ setSelectedTab(Tab.All) }} + > + All Binaries +
+
{ setSelectedTab(Tab.My) }} + > + My Binaries +
+
+ ) +} \ No newline at end of file diff --git a/components/wallet/index.tsx b/components/wallet/index.tsx index 186ffbe..ec72ec9 100644 --- a/components/wallet/index.tsx +++ b/components/wallet/index.tsx @@ -1,70 +1,80 @@ -import { Inter } from 'next/font/google' -import { useEffect, useState } from 'react'; -import { isAllowed, setAllowed, getUserInfo, getPublicKey, isConnected, getNetwork } from '@stellar/freighter-api'; -import { StateVariablesProps } from '@/pages'; -import styles from './style.module.css'; -import { useThemeContext } from '../ThemeContext' +import { Roboto } from 'next/font/google' +import { use, useEffect, useState } from 'react' +import { isAllowed, setAllowed, getUserInfo, getPublicKey, isConnected, getNetwork } from '@stellar/freighter-api' +import styles from './style.module.css' +import { useThemeContext } from '../../context/ThemeContext' +import { useWalletContext } from '../../context/WalletContext' -const inter = Inter({ subsets: ['latin'] }) +const roboto = Roboto({ weight: ['400', '700'], subsets: ['latin'] }) -export default function WalletInfo(props: StateVariablesProps) { +export default function WalletInfo() { // Import the current Theme const { activeTheme } = useThemeContext(); - const [showWallet, setShowWallet] = useState(false); + // Import the wallet Context + const walletContext = useWalletContext(); async function connect() { const freighterConnected = await isConnected(); if (!freighterConnected) { - props.walletInfo.setHasFreighter(false); + walletContext.setHasFreighter(false); } else { await setAllowed(); if (await isAllowed()) { - const publicKey = await getPublicKey(); - const network = await getNetwork(); - props.walletInfo.setAddress(publicKey); - props.walletInfo.setNetwork(network); - props.walletInfo.setConnected(true); + walletContext.setAddress(await getPublicKey()); + walletContext.setNetwork(await getNetwork()); + walletContext.setConnected(true); } } } async function stayConnected() { if (await isAllowed()) { - const publicKey = await getUserInfo(); - const network = await getNetwork(); - props.walletInfo.setAddress(publicKey.publicKey); - props.walletInfo.setNetwork(network); + walletContext.setConnected(true); } } useEffect(() => { - const timer = setTimeout(() => { - setShowWallet(true); - }, 1000); stayConnected(); - return () => clearTimeout(timer); - }, []); + }, []) + async function refetchWalletInfo() { + const userInfo = await getUserInfo(); + if (userInfo.publicKey != "") { + walletContext.setAddress(userInfo.publicKey); + walletContext.setNetwork(await getNetwork()); + walletContext.setConnected(true); + } else { + walletContext.setConnected(false); + } + } + + useEffect(() => { + const interval = setInterval(() => { + if (walletContext.connected) { + refetchWalletInfo(); + } + }, 1500); + + return () => clearInterval(interval); + }, [walletContext.connected]) return ( -
- {showWallet && ( - <> - {!props.walletInfo.address && props.walletInfo.hasFreighter ? ( +
+ <> + {(!walletContext.address && walletContext.hasFreighter) || (!walletContext.connected) ? ( - ) : !props.walletInfo.hasFreighter ? ( + ) : !walletContext.hasFreighter ? (

You don't have Freighter extension

) : ( <> -
{props.walletInfo.network}
-
{props.walletInfo.address.substring(0, 4) + "..." + props.walletInfo.address.slice(-4)}
+
{walletContext.network}
+
{walletContext.address.substring(0, 4) + "..." + walletContext.address.slice(-4)}
)} - - )} +
) } \ No newline at end of file diff --git a/components/ThemeContext.tsx b/context/ThemeContext.tsx similarity index 100% rename from components/ThemeContext.tsx rename to context/ThemeContext.tsx diff --git a/context/TimeToLiveContext.tsx b/context/TimeToLiveContext.tsx new file mode 100644 index 0000000..a4fb010 --- /dev/null +++ b/context/TimeToLiveContext.tsx @@ -0,0 +1,38 @@ +import { createContext, useContext, ReactNode, Dispatch, SetStateAction, useState } from 'react'; + +export interface TimeToLive { + automaticBump: boolean; + date: string; + ttlSec?: number; + countdown?: String; +} + +export type TimeToLiveType = { + addressToTtl: Map; + setAddressToTtl: Dispatch>> +} + +type TimeToLiveProviderProps = { + children: ReactNode; +}; + +const TimeToLiveContext = createContext(undefined); + +export const TimeToLiveContextProvider: React.FC = ({ children }) => { + + const [addressToTtl, setAddressToTtl] = useState>(new Map()); + + return ( + + {children} + + ); +}; + +export const useTimeToLiveContext = (): TimeToLiveType => { + const context = useContext(TimeToLiveContext); + if (!context) { + throw new Error('useThemeContext must be used within a TimeToLiveContextProvider'); + } + return context; +}; \ No newline at end of file diff --git a/context/WalletContext.tsx b/context/WalletContext.tsx new file mode 100644 index 0000000..fb0e6ac --- /dev/null +++ b/context/WalletContext.tsx @@ -0,0 +1,51 @@ +import { createContext, useContext, useState, ReactNode, Dispatch, SetStateAction } from 'react' + +export type WalletContextType = { + hasFreighter: boolean; + setHasFreighter: Dispatch>; + connected: boolean; + setConnected: Dispatch>; + network: string; + setAddress: Dispatch>; + address: string; + setNetwork: Dispatch>; +} + +type WalletInfoContextProviderProps = { + children: ReactNode; +} + +const WalletContext = createContext(undefined); + +export const WalletContextProvider: React.FC = ({ children }) => { + + const [hasFreighter, setHasFreighter] = useState(true); + const [connected, setConnected] = useState(false); + const [network, setNetwork] = useState(""); + const [address, setAddress] = useState(""); + + return ( + + {children} + + ); +} + +export const useWalletContext = (): WalletContextType => { + const context = useContext(WalletContext); + if (!context) { + throw new Error('useWalletContext must be used within a WalletContextProvider'); + } + return context; +}; \ No newline at end of file diff --git a/contract_id.txt b/contract_id.txt index f70826b..55744da 100644 --- a/contract_id.txt +++ b/contract_id.txt @@ -1 +1 @@ -CDNOMEB3ZQHS5WPCUPQ7IS4OKGTOTBRDCZUITBRNSQAB63JJ52JFO4KX \ No newline at end of file +CBXSF7FVBQNLQLNWZLPB7RIVMHBGTEZEWKBELZAX3PKZDSCROIXMV4LA \ No newline at end of file diff --git a/endpoints.config.ts b/endpoints.config.ts new file mode 100644 index 0000000..bd1ab6b --- /dev/null +++ b/endpoints.config.ts @@ -0,0 +1,13 @@ +const endpoints = { + publish_events: process.env.NEXT_PUBLIC_PUBLISH_EVENTS_ENDPOINT ?? "", + deploy_events: process.env.NEXT_PUBLIC_DEPLOY_EVENTS_ENDPOINT ?? "", + claim_events: process.env.NEXT_PUBLIC_CLAIM_EVENTS_ENDPOINT ?? "", + subscribe_ledger_expiration: process.env.NEXT_PUBLIC_SUBSCRIBE_LEDGER_EXPIRATION_ENDPOINT ?? "", + read_ttl: process.env.NEXT_PUBLIC_READ_LEDGER_TTL_ENDPOINT ?? "", + bump_contract_instance: process.env.NEXT_PUBLIC_BUMP_CONTRACT_INSTANCE_ENDPOINT ?? "", + postgresql_endpoint: process.env.NEXT_PUBLIC_POSTGRESQL_ENDPOINT ?? "", + rpc_endpoint: process.env.NEXT_PUBLIC_STELLAR_RPC_ENDPOINT ?? "", +} + +export default endpoints; + \ No newline at end of file diff --git a/mercury_indexer/smartdeploy-api-client.ts b/mercury_indexer/smartdeploy-api-client.ts new file mode 100644 index 0000000..e0abec5 --- /dev/null +++ b/mercury_indexer/smartdeploy-api-client.ts @@ -0,0 +1,279 @@ +import { useQuery } from '@tanstack/react-query' +import axios from 'axios'; +import endpoints from '@/endpoints.config'; +import { Version, Update, ContractMetadata } from 'smartdeploy-client' + +export interface PublishEventData { + publishedName: string; + author: string; + hash: string; + repo: ContractMetadata; + kind: Update; + [key: string]: string | ContractMetadata | Update; +} + +export function getPublishEvents() { + + // Fetch publish events data every 5 seconds + const { data } = useQuery({ + queryKey: ['publish_events'], + queryFn: async () => { + try { + const res = await axios.get(endpoints.publish_events); + + var publishEvents: PublishEventData[] = []; + + ///@ts-ignore + res.data.forEach((publishEvent) => { + + const parsedPublishEvent: PublishEventData = { + publishedName: "", + author: "", + hash: "", + repo: { repo: ""}, + kind: { tag: "Patch", values: undefined}, + } + + ///@ts-ignore + publishEvent.map.forEach((eventField) => { + var symbol: string = eventField.key.symbol; + + if (symbol === 'author') { + parsedPublishEvent.author = eventField.val.address; + } else if (symbol === 'hash') { + parsedPublishEvent.hash = eventField.val.bytes; + } else if (symbol === 'kind') { + parsedPublishEvent.kind = { tag: eventField.val.vec[0].symbol, values: undefined} as Update; + } else if (symbol === 'published_name') { + parsedPublishEvent.publishedName = eventField.val.string; + } else { + parsedPublishEvent.repo = { repo: eventField.val.map[0].val.string } as ContractMetadata; + } + + }) + publishEvents.push(parsedPublishEvent); + }) + return publishEvents; + + } catch (error) { + console.error("Error to get the Publish events", error); + + } + }, + refetchInterval: 5000, + + }); + + return data; +} + +export interface DeployEventData { + publishedName: string; + deployedName: string; + version: Version; + deployer: string; + contractId: string; + [key: string]: string | Version; +} + +export function getDeployEvents() { + + // Fetch deploy events data every 5 seconds + const { data } = useQuery({ + queryKey: ['deploy_events'], + queryFn: async () => { + try { + const res = await axios.get(endpoints.deploy_events); + + var deployEvents: DeployEventData[] = []; + + ///@ts-ignore + res.data.forEach((deployEvent) => { + + const parsedDeployEvent: DeployEventData = { + publishedName: "", + deployedName: "", + version: { major: 0, minor: 0, patch: 0 }, + deployer: "", + contractId: "", + } + + ///@ts-ignore + deployEvent.map.forEach((eventField) => { + var symbol: string = eventField.key.symbol; + + if (symbol == "contract_id") { + parsedDeployEvent.contractId = eventField.val.address; + } else if (symbol === 'deployed_name') { + symbol = 'deployedName'; + parsedDeployEvent.deployedName = eventField.val.string; + } else if (symbol === 'deployer') { + parsedDeployEvent.deployer = eventField.val.address; + } else if (symbol ==='version') { + ///@ts-ignore + eventField.val.map.forEach((versionField) => { + var versionSymbol: string = versionField.key.symbol; + if (versionSymbol ==='major') { + parsedDeployEvent.version.major = versionField.val.u32; + } else if (versionSymbol ==='minor') { + parsedDeployEvent.version.minor = versionField.val.u32; + } else { + parsedDeployEvent.version.patch = versionField.val.u32; + } + }); + } else { + parsedDeployEvent.publishedName = eventField.val.string; + } + + }) + deployEvents.push(parsedDeployEvent); + }) + return deployEvents; + + } catch (error) { + console.error("Error to get the Deploy events", error); + } + }, + refetchInterval: 5000, + }); + + return data; +} + +export interface ClaimEventData { + deployedName: string; + claimer: string; + contractId: string; + wasmHash: string; + [key: string]: string | Version; +} + +export function getClaimEvents() { + + // Fetch deploy events data every 5 seconds + const { data } = useQuery({ + queryKey: ['claim_events'], + queryFn: async () => { + try { + const res = await axios.get(endpoints.claim_events); + + var claimEvents: ClaimEventData[] = []; + + ///@ts-ignore + res.data.forEach((claimEvent) => { + + const parsedClaimEvent: ClaimEventData = { + deployedName: "", + claimer: "", + contractId: "", + wasmHash: claimEvent[1], + } + + ///@ts-ignore + claimEvent[0].map.forEach((eventField) => { + + var symbol: string = eventField.key.symbol; + + if (symbol == "contract_id") { + parsedClaimEvent.contractId = eventField.val.address; + } else if (symbol === 'deployed_name') { + symbol = 'deployedName'; + parsedClaimEvent.deployedName = eventField.val.string; + } else { + parsedClaimEvent.claimer = eventField.val.address; + } + + }) + claimEvents.push(parsedClaimEvent); + }) + return claimEvents; + + } catch (error) { + console.error("Error to get the Claim events", error); + } + }, + refetchInterval: 5000, + }); + + return data; +} + +// We don't use that function because we calculate ourself the TTL +// Mercury expiration tracking too tricky to use +export async function subscribeBump(id: String) { + + const url = endpoints.subscribe_ledger_expiration + '/' + id; + + try { + const res = await axios.get(url); + return res.data; + } catch (error) { + console.error("Error to subscribe to Ledger Instance Expiration:", error); + window.alert("Error to subscribe to Ledger Instance Expiration. The problem comes from the Smart Deploy API"); + return 0; + } + +} + +export async function readTtl(id: String) { + + const url = endpoints.read_ttl + '/' + id; + + try { + const res = await axios.get(url); + return res.data; + } catch (error) { + console.error("Error to read Ledger Instance Expiration:", error); + window.alert("Error to read Ledger Instance Expiration. The problem comes from the Smart Deploy API"); + return 0; + } + +} + +export async function bumpContractInstance(contract_id: String, ledgers_to_extend: number) { + + const url = endpoints.bump_contract_instance + '/' + contract_id + '/' + ledgers_to_extend; + + try { + const res = await axios.get(url); + return res.data; + + } catch (error) { + console.error("Error to extend Ledger Instance:", error); + window.alert("Error to bump Ledger Instance. The problem comes from the Smart Deploy API"); + return 0; + } +} + +export async function addDbTtlData( + contractId: string, + automaticBump: boolean, + liveUntilTtl: number, +) { + + const ttl_Data = { + contract_id: contractId, + automatic_bump: automaticBump, + live_until_ttl: liveUntilTtl + } + + try { + await axios.post(endpoints.postgresql_endpoint, ttl_Data); + + } catch (error) { + console.error("Error to add data to postgres DB", error); + window.alert("Adding bumping data to postgres DB failed. The problem comes from the Smart Deploy API"); + return 0; + } + +} + +export async function fetchTtlContractsData() { + try { + const res = await axios.get(endpoints.postgresql_endpoint); + return res.data; + } catch (error) { + console.error("Error to fetch data from postgres DB", error); + window.alert("Fetching bumping data from postgres DB failed. The problem comes from the Smart Deploy API"); + } +} \ No newline at end of file diff --git a/next.config.js b/next.config.js index 81908b4..7b3210a 100644 --- a/next.config.js +++ b/next.config.js @@ -1,9 +1,6 @@ module.exports = { output: "export", basePath: process.env.NODE_ENV === "production" ? "": undefined, - experimental: { - appDir: true, - }, images: { unoptimized: true, }, diff --git a/package-lock.json b/package-lock.json index 58b3d4a..c98bb63 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,8 +8,14 @@ "name": "frontend", "version": "0.1.0", "hasInstallScript": true, + "workspaces": [ + "packages/*" + ], "dependencies": { "@stellar/freighter-api": "^1.7.1", + "@tanstack/react-query": "^5.20.5", + "axios": "^1.6.7", + "date-fns": "^3.3.1", "next": "13.5.4", "react": "^18", "react-dom": "^18", @@ -170,6 +176,41 @@ "resolved": "https://registry.npmjs.org/@stellar/freighter-api/-/freighter-api-1.7.1.tgz", "integrity": "sha512-XvPO+XgEbkeP0VhP0U1edOkds+rGS28+y8GRGbCVXeZ9ZslbWqRFQoETAdX8IXGuykk2ib/aPokiLc5ZaWYP7w==" }, + "node_modules/@stellar/js-xdr": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@stellar/js-xdr/-/js-xdr-3.1.0.tgz", + "integrity": "sha512-mYTyFnhgyQgyvpAYZRO1LurUn2MxcIZRj74zZz/BxKEk7zrL4axhQ1ez0HL2BRi0wlG6cHn5BeD/t9Xcyp7CSQ==" + }, + "node_modules/@stellar/stellar-base": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@stellar/stellar-base/-/stellar-base-10.0.1.tgz", + "integrity": "sha512-BDbx7VHOEQh+4J3Q+gStNXgPaNckVFmD4aOlBBGwxlF6vPFmVnW8IoJdkX7T58zpX55eWI6DXvEhDBlrqTlhAQ==", + "dependencies": { + "@stellar/js-xdr": "^3.0.1", + "base32.js": "^0.1.0", + "bignumber.js": "^9.1.2", + "buffer": "^6.0.3", + "sha.js": "^2.3.6", + "tweetnacl": "^1.0.3" + }, + "optionalDependencies": { + "sodium-native": "^4.0.1" + } + }, + "node_modules/@stellar/stellar-sdk": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/@stellar/stellar-sdk/-/stellar-sdk-11.2.0.tgz", + "integrity": "sha512-qInRR+mLLl9O/AI6Q+Sr19RZeYJtlNoJQJi3pch5BYoMvVhjO8IU8AhHADP//Zmc2osyogwPuqXBiFdaGlfHWA==", + "dependencies": { + "@stellar/stellar-base": "10.0.1", + "axios": "^1.6.5", + "bignumber.js": "^9.1.2", + "eventsource": "^2.0.2", + "randombytes": "^2.1.0", + "toml": "^3.0.0", + "urijs": "^1.19.1" + } + }, "node_modules/@swc/helpers": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.2.tgz", @@ -178,10 +219,34 @@ "tslib": "^2.4.0" } }, + "node_modules/@tanstack/query-core": { + "version": "5.20.5", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.20.5.tgz", + "integrity": "sha512-T1W28gGgWn0A++tH3lxj3ZuUVZZorsiKcv+R50RwmPYz62YoDEkG4/aXHZELGkRp4DfrW07dyq2K5dvJ4Wl1aA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.20.5", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.20.5.tgz", + "integrity": "sha512-6MHwJ8G9cnOC/XKrwt56QMc91vN7hLlAQNUA0ubP7h9Jj3a/CmkUwT6ALdFbnVP+PsYdhW3WONa8WQ4VcTaSLQ==", + "dependencies": { + "@tanstack/query-core": "5.20.5" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18.0.0" + } + }, "node_modules/@types/node": { - "version": "20.8.10", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.10.tgz", - "integrity": "sha512-TlgT8JntpcbmKUFzjhsyhGfP2fsiz1Mv56im6enJ905xG1DAYesxJaeSbGqQmAw8OWPdhyJGhGSQGKRNJ45u9w==", + "version": "20.11.19", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.19.tgz", + "integrity": "sha512-7xMnVEcZFu0DikYjWOlRq7NTPETrm7teqUT2WkQjrTIkEgUyyGdWsj/Zg8bEJt5TNklzbPD1X3fqfsHw3SpapQ==", "dev": true, "dependencies": { "undici-types": "~5.26.4" @@ -225,6 +290,79 @@ "integrity": "sha512-+GQpwVGd7YzY3M5rR/nKZTt1PALu01olJCV+KOTUJ1IBPz8P0BV0WBM8rrc2qPwJ6252Mpp5X6uJXWD7MV0ISQ==", "dev": true }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.7.tgz", + "integrity": "sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==", + "dependencies": { + "follow-redirects": "^1.15.4", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/base32.js": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/base32.js/-/base32.js-0.1.0.tgz", + "integrity": "sha512-n3TkB02ixgBOhTvANakDb4xaMXnYUVkNoRFJjQflcqMQhyEKxEHdj3E6N8t8sUQ0mjH/3/JxzlXuz3ul/J90pQ==", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/bignumber.js": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", + "integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==", + "engines": { + "node": "*" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, "node_modules/busboy": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", @@ -237,9 +375,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001559", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001559.tgz", - "integrity": "sha512-cPiMKZgqgkg5LY3/ntGeLFUpi6tzddBNS58A4tnTgQw1zON7u2sZMU7SzOeVH4tj20++9ggL+V6FDOFMTaFFYA==", + "version": "1.0.30001588", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001588.tgz", + "integrity": "sha512-+hVY9jE44uKLkH0SrUTqxjxqNTOWHsbnQDIKjwkZ3lNTzUUVdBLBGXtj/q5Mp5u98r3droaZAewQuEDzjQdZlQ==", "funding": [ { "type": "opencollective", @@ -265,12 +403,80 @@ "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/csstype": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==", "dev": true }, + "node_modules/date-fns": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.3.1.tgz", + "integrity": "sha512-y8e109LYGgoQDveiEBD3DYXKba1jWf5BA8YU1FL5Tvm0BTdEfy54WLCwnuYWZNnzzvALy/QQ4Hov+Q9RVRv+Zw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/eventsource": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-2.0.2.tgz", + "integrity": "sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", + "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/glob-to-regexp": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", @@ -281,6 +487,30 @@ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -297,6 +527,25 @@ "loose-envify": "cli.js" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/nanoid": { "version": "3.3.6", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", @@ -359,6 +608,17 @@ } } }, + "node_modules/node-gyp-build": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.0.tgz", + "integrity": "sha512-u6fs2AEUljNho3EYTJNBfImO5QTo/J/1Etd+NVdCj7qWKUSN/bSLkZwhDv7I+w/MSC6qJ4cknepkAYykDdK8og==", + "optional": true, + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -391,6 +651,19 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, "node_modules/react": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", @@ -446,6 +719,25 @@ "react-dom": ">=16" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/scheduler": { "version": "0.23.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", @@ -454,6 +746,32 @@ "loose-envify": "^1.1.0" } }, + "node_modules/sha.js": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "dependencies": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + }, + "bin": { + "sha.js": "bin.js" + } + }, + "node_modules/smartdeploy-client": { + "resolved": "packages/smartdeploy-client", + "link": true + }, + "node_modules/sodium-native": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-4.0.10.tgz", + "integrity": "sha512-vrJQt4gASntDbnltRRk9vN4rks1SehjM12HkqQtu250JtWT+/lo8oEOa1HvSq3+8hzJdYcCJuLR5qRGxoRDjAg==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "node-gyp-build": "^4.6.0" + } + }, "node_modules/source-map-js": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", @@ -492,15 +810,25 @@ } } }, + "node_modules/toml": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/toml/-/toml-3.0.0.tgz", + "integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==" + }, "node_modules/tslib": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, + "node_modules/tweetnacl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", + "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==" + }, "node_modules/typescript": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", - "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", + "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -516,6 +844,11 @@ "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", "dev": true }, + "node_modules/urijs": { + "version": "1.19.11", + "resolved": "https://registry.npmjs.org/urijs/-/urijs-1.19.11.tgz", + "integrity": "sha512-HXgFDgDommxn5/bIv0cnQZsPhHDA90NPHD6+c/v21U5+Sx5hoP8+dP9IZXBU1gIfvdRfhG8cel9QNPeionfcCQ==" + }, "node_modules/watchpack": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", @@ -527,6 +860,17 @@ "engines": { "node": ">=10.13.0" } + }, + "packages/smartdeploy-client": { + "version": "0.0.0", + "dependencies": { + "@stellar/freighter-api": "1.7.1", + "@stellar/stellar-sdk": "11.2.0", + "buffer": "6.0.3" + }, + "devDependencies": { + "typescript": "5.3.3" + } } } } diff --git a/package.json b/package.json index ee96d4d..07893f4 100644 --- a/package.json +++ b/package.json @@ -3,16 +3,19 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "next dev", - "build": "next build", + "dev": "npm run bindings && next dev", + "build": "npm run bindings && next build", "start": "next start", "lint": "next lint", - "postinstall": "just && npm run bindings", + "postinstall": "just", "soroban": "./target/bin/soroban", - "bindings": "npm run soroban -- contract bindings typescript --overwrite --network testnet --contract-id $(cat contract_id.txt) --output-dir node_modules/smartdeploy-client" + "bindings": "npm run soroban -- contract bindings typescript --overwrite --network testnet --contract-id $(cat contract_id.txt) --output-dir packages/smartdeploy-client" }, "dependencies": { "@stellar/freighter-api": "^1.7.1", + "@tanstack/react-query": "^5.20.5", + "axios": "^1.6.7", + "date-fns": "^3.3.1", "next": "13.5.4", "react": "^18", "react-dom": "^18", @@ -26,5 +29,8 @@ "@types/react-dom": "^18", "@types/uikit": "^3.14.1", "typescript": "^5" - } + }, + "workspaces": [ + "packages/*" + ] } diff --git a/packages/.gitkeep b/packages/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/pages/_app.tsx b/pages/_app.tsx index 1636de3..5890cd7 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -1,11 +1,22 @@ import '@/styles/globals.css' -import { ThemeContextProvider } from '../components/ThemeContext' +import { ThemeContextProvider } from '../context/ThemeContext' +import { WalletContextProvider } from '../context/WalletContext' +import { TimeToLiveContextProvider } from '@/context/TimeToLiveContext' import type { AppProps } from 'next/app' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' + +const queryClient = new QueryClient(); export default function App({ Component, pageProps }: AppProps) { return ( - + + + + + + + ) } diff --git a/pages/index.tsx b/pages/index.tsx index 46777f1..f6b6ca9 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -1,89 +1,60 @@ import Head from 'next/head' import Image from 'next/image' -import { Inter } from 'next/font/google' +import { Roboto } from 'next/font/google' import { useState, Dispatch, SetStateAction } from 'react' import styles from '@/styles/Home.module.css' import WalletInfo from '@/components/wallet' import PublishedTab from '@/components/published-tab' import DeployedTab from '@/components/deployed-tab' import PopupDappInfo from '@/components/dapp-info-popup' -import { FaDiscord, FaTwitter, FaGithub } from "react-icons/fa"; +import { + getDeployEvents, DeployEventData, + getPublishEvents, PublishEventData, + getClaimEvents, ClaimEventData, +} from '@/mercury_indexer/smartdeploy-api-client' +import { FaDiscord, FaTwitter, FaGithub } from "react-icons/fa" import { BsFillSunFill } from 'react-icons/bs' import { MdNightlightRound } from 'react-icons/md' -import { Contract, networks } from 'smartdeploy-client'; -import { useThemeContext } from '../components/ThemeContext' +import { Contract, networks } from 'smartdeploy-client' +import { useThemeContext } from '../context/ThemeContext' -const inter = Inter({ subsets: ['latin'] }) +const roboto = Roboto({ weight: ['400', '700'], subsets: ['latin'] }) // Smartdeploy Contract Instance export const smartdeploy = new Contract({ - networkPassphrase: "Test SDF Network ; September 2015", - contractId: "CDNOMEB3ZQHS5WPCUPQ7IS4OKGTOTBRDCZUITBRNSQAB63JJ52JFO4KX", + ...networks.testnet, rpcUrl: 'https://soroban-testnet.stellar.org:443', }); -export type UserWalletInfo = { - connected: boolean; - setConnected: Dispatch>; - hasFreighter: boolean; - setHasFreighter: Dispatch>; - address: string; - setAddress: Dispatch>; - network: string; - setNetwork: Dispatch>; -} - -export type FetchDatas = { - fetch: boolean; - setFetch: Dispatch>; -} - -export type StateVariablesProps = { - walletInfo: UserWalletInfo; - fetchDeployed?: FetchDatas; - fetchPublished?: FetchDatas; -} - export default function Home() { // Import the current Theme const { activeTheme, setActiveTheme, inactiveTheme } = useThemeContext(); - // State variables from Freighter Wallet - const [connected, setConnected] = useState(false); - const [hasFreighter, setHasFreighter] = useState(true); - const [address, setAddress] = useState(""); - const [network, setNetwork] = useState(""); + const [deployEvents, setDeployEvents] = useState([]); + const [publishEvents, setPublishEvents] = useState([]); + const [claimEvents, setClaimEvents] = useState([]); - // Parse state variables regarding the Freighter's infos - const userWalletInfo: UserWalletInfo = { - connected, - setConnected, - hasFreighter, - setHasFreighter, - address, - setAddress, - network, - setNetwork, + let newPublishEvents = getPublishEvents(); + // Update publishEvents if necessary + if (newPublishEvents != publishEvents) { + setPublishEvents(newPublishEvents); } + console.log("PUBLISH EVENTS: ", publishEvents) - // State variable to fetch the published contracts - const [fetchPublishedContracts, setFetchPublishedContracts] = useState(true); - - // Parse state variable for fetching the deployed contracts - const parsedFetchPublishedContracts: FetchDatas = { - fetch: fetchPublishedContracts, - setFetch: setFetchPublishedContracts, + let newDeployEvents = getDeployEvents(); + // Update deployEvents if necessary + if (newDeployEvents != deployEvents) { + setDeployEvents(newDeployEvents); } + console.log("DEPLOY EVENTS: ", deployEvents) - // State variable to fetch the deployed contracts - const [fetchDeployedContracts, setFetchDeployedContracts] = useState(true); - - // Parse state variable for fetching the deployed contracts - const parsedFetchDeployedContracts: FetchDatas = { - fetch: fetchDeployedContracts, - setFetch: setFetchDeployedContracts, + let newClaimEvents = getClaimEvents(); + // Update deployEvents if necessary + if (newClaimEvents != claimEvents) { + setClaimEvents(newClaimEvents); } + console.log("CLAIM EVENTS: ", claimEvents) return ( <> @@ -94,7 +65,7 @@ export default function Home() { -
+
{activeTheme === 'dark' ? ( @@ -148,7 +119,7 @@ export default function Home() {
- + { activeTheme === "dark" ? (
-
+
{ activeTheme === "dark" ? ( @@ -193,8 +164,8 @@ export default function Home() {
- - + +