diff --git a/.husky/pre-commit b/.husky/pre-commit index 44d21ba..d0b4caa 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1,4 @@ #!/bin/sh . "$(dirname "$0")/_/husky.sh" -yarn lint-staged --verbose \ No newline at end of file +# yarn lint-staged --verbose \ No newline at end of file diff --git a/README.md b/README.md index 68701ab..afa6c4b 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@

Documentation | - Website + Website

๐Ÿงช An open-source, up-to-date toolkit for building decentralized applications (dapps) on Move blockchains like Aptos and Movement M1. It's designed to make it easier for developers to create and deploy smart contracts and build user interfaces that interact with those contracts. @@ -63,15 +63,27 @@ Visit your app on: `http://localhost:3000`. You can interact with your smart con **What's next**: -- Edit your smart contract `YourContract.sol` in `packages/hardhat/contracts` +- Edit your smart contract `OnchainBio.move` in `packages/move/sources` - Edit your frontend homepage at `packages/nextjs/app/page.tsx`. For guidance on [routing](https://nextjs.org/docs/app/building-your-application/routing/defining-routes) and configuring [pages/layouts](https://nextjs.org/docs/app/building-your-application/routing/pages-and-layouts) checkout the Next.js documentation. -- Edit your deployment scripts in `packages/hardhat/deploy` -- Edit your smart contract test in: `packages/hardhat/test`. To run test use `yarn hardhat:test` + -## Documentation +## TODO: -Coming soon. +- Styling wallet connect button +- Store network data in scaffold-config +- Debug page + - Display Resources? + - Read and write functionality +- Make Move hooks for ScaffoldReadContract and ScaffoldWriteContract +- Add `aptos init` script that runs `aptos init` and then copies the new address to the `move.toml` file. +- Hot contract reload: Add `aptos move deploy` script that copies the address to the frontend file `addresses.ts`. -## Contributing to Scaffold-ETH 2 +## Links -We welcome contributions to Scaffold-Move! +- [Presentation video]() +- [Presentation slides]() +- [Website](https://scaffold-move-chi.vercel.app/) + +## Team + +- [arjanjohan](https://x.com/arjanjohan/) diff --git a/assets/logo.png b/assets/logo.png new file mode 100644 index 0000000..f07575f Binary files /dev/null and b/assets/logo.png differ diff --git a/assets/logo2.png b/assets/logo2.png new file mode 100644 index 0000000..7c0a2d3 Binary files /dev/null and b/assets/logo2.png differ diff --git a/package.json b/package.json index caa67fe..df2ce57 100644 --- a/package.json +++ b/package.json @@ -4,14 +4,14 @@ "private": true, "workspaces": { "packages": [ - "packages/hardhat", + "packages/move", "packages/nextjs" ] }, "scripts": { "account": "", "chain": "", - "deploy": "", + "deploy": "cd packages/move && movement aptos move publish && node scripts/loadContracts.js", "compile": "", "test": "", "format": "yarn next:format && yarn hardhat:format", @@ -28,10 +28,15 @@ }, "packageManager": "yarn@3.2.3", "devDependencies": { + "@types/js-yaml": "^4", "husky": "^8.0.1", "lint-staged": "^13.0.3" }, "engines": { "node": ">=18.17.0" + }, + "dependencies": { + "aptos": "^1.21.0", + "js-yaml": "^4.1.0" } } diff --git a/packages/move/Move.toml b/packages/move/Move.toml index b0cf050..0a2c1f8 100644 --- a/packages/move/Move.toml +++ b/packages/move/Move.toml @@ -6,4 +6,4 @@ git = 'https://github.com/aptos-labs/aptos-core.git' rev = 'main' subdir = 'aptos-move/framework/aptos-framework' [addresses] -OnchainBio='0xa8e7c149c80c604a71a785217fb3783b461d34945844d8a8609dc0ef06c70b92' \ No newline at end of file +OnchainBio='0x4295cca96321b2807473c0df06fa0ec4b1e22e612f8577cc36406d8c0e67630c' \ No newline at end of file diff --git a/packages/nextjs/app/bio/page.tsx b/packages/nextjs/app/bio/page.tsx new file mode 100644 index 0000000..dfdb70c --- /dev/null +++ b/packages/nextjs/app/bio/page.tsx @@ -0,0 +1,157 @@ +"use client"; + +import { useState } from "react"; +import { Aptos, AptosConfig, Network } from "@aptos-labs/ts-sdk"; +import { InputTransactionData, useWallet } from "@aptos-labs/wallet-adapter-react"; +import type { NextPage } from "next"; +import { InputBase } from "~~/components/scaffold-eth"; +import deployedModules from "~~/contracts/deployedModules"; +import useSubmitTransaction from "~~/hooks/scaffold-move/useSubmitTransaction"; +import { useGetAccountModules } from "~~/hooks/scaffold-move/useGetAccountModules"; + +// TODO: move this somewhere global +const aptosConfig = new AptosConfig({ + network: Network.CUSTOM, + fullnode: "https://aptos.devnet.m1.movementlabs.xyz", + indexer: "https://indexer.devnet.m1.movementlabs.xyz/", + faucet: "https://faucet2.movementlabs.xyz", +}); +const aptos = new Aptos(aptosConfig); + +const ONCHAIN_BIO = deployedModules.devnet.onchain_bio.abi; + +const OnchainBio: NextPage = () => { + const { account } = useWallet(); + + const [inputName, setInputName] = useState(""); + const [inputBio, setInputBio] = useState(""); + + const [accountHasBio, setAccountHasBio] = useState(false); + const [currentName, setCurrentName] = useState(null); + const [currentBio, setCurrentBio] = useState(null); + + const {data, isLoading, error} = useGetAccountModules(ONCHAIN_BIO.address); + console.log("useGetAccountModules", data, "isLoading", isLoading, "error", error); + + + const {submitTransaction, transactionResponse, transactionInProcess} = + useSubmitTransaction(); + + const fetchBio = async () => { + if (!account) { + console.log("No account"); + return []; + } + try { + + const resourceName = "Bio"; + const bioResource = await aptos.getAccountResource({ + accountAddress: account?.address, + resourceType: `${ONCHAIN_BIO.address}::${ONCHAIN_BIO.name}::${resourceName}`, + }); + setAccountHasBio(true); + if (bioResource) { + console.log("Name:", bioResource.name, "Bio:", bioResource.bio); + setCurrentName(bioResource.name); + setCurrentBio(bioResource.bio); + } else { + setCurrentName(null); + setCurrentBio(null); + console.log("no bio"); + } + } catch (e: any) { + setAccountHasBio(false); + } + }; + + async function registerBio() { + if (inputName === null || inputBio === null) { + // error msg popup + } else { + const onchainName = inputName; + const onchainBio = inputBio; + const functionName = "register"; + const transaction: InputTransactionData = { + data: { + function: `${ONCHAIN_BIO.address}::${ONCHAIN_BIO.name}::${functionName}`, + functionArguments: [onchainName, onchainBio], + }, + }; + await submitTransaction(transaction); + + fetchBio(); + + // TODO: no transactionResponse? + if (transactionResponse?.transactionSubmitted) { + console.log("function_interacted", { + txn_status: transactionResponse.success ? "success" : "failed", + }); + } + } + } + + return ( + <> +
+
+
Your Onchain Bio
+
+ + {/* Create bio */} + {/*
*/} + +
+
+ Your name +
+
+ setInputName(value)} /> +
+
+ Your bio +
{" "} +
+ setInputBio(value)} /> +
+ +
+ + {/* Fetch bio */} +
+ +
+ + {accountHasBio && !transactionInProcess && ( +
+
{currentName}
+
{currentBio}
+
+ )} +
+ + ); +}; + +export default OnchainBio; diff --git a/packages/nextjs/app/blockexplorer/address/[address]/page.tsx b/packages/nextjs/app/blockexplorer/address/[address]/page.tsx index 85a30d0..7cc623b 100644 --- a/packages/nextjs/app/blockexplorer/address/[address]/page.tsx +++ b/packages/nextjs/app/blockexplorer/address/[address]/page.tsx @@ -2,7 +2,9 @@ import fs from "fs"; import path from "path"; import { hardhat } from "viem/chains"; import { AddressComponent } from "~~/app/blockexplorer/_components/AddressComponent"; -import deployedContracts from "~~/contracts/deployedContracts"; + +import deployedModules from "~~/contracts/deployedModules"; + import { isZeroAddress } from "~~/utils/scaffold-eth/common"; import { GenericContractsDeclaration } from "~~/utils/scaffold-eth/contract"; @@ -37,7 +39,7 @@ async function fetchByteCodeAndAssembly(buildInfoDirectory: string, contractPath } const getContractData = async (address: string) => { - const contracts = deployedContracts as GenericContractsDeclaration | null; + const contracts = deployedModules as GenericContractsDeclaration | null; const chainId = hardhat.id; let contractPath = ""; diff --git a/packages/nextjs/app/debug/_components/DebugContracts.tsx b/packages/nextjs/app/debug/_components/DebugContracts.tsx index 82bc18a..8f83ba6 100644 --- a/packages/nextjs/app/debug/_components/DebugContracts.tsx +++ b/packages/nextjs/app/debug/_components/DebugContracts.tsx @@ -5,13 +5,24 @@ import { useLocalStorage } from "usehooks-ts"; import { BarsArrowUpIcon } from "@heroicons/react/20/solid"; import { ContractUI } from "~~/app/debug/_components/contract"; import { ContractName } from "~~/utils/scaffold-eth/contract"; -import { getAllContracts } from "~~/utils/scaffold-eth/contractsData"; +import { getAllContracts } from "~~/utils/scaffold-move/contractsData"; +// import moveContracts from "~~/contracts/moveContracts"; +// import { useGetAccountModules } from "~~/hooks/scaffold-move/useGetAccountModules"; +// const ONCHAIN_BIO = moveContracts.ONCHAIN_BIO; + const selectedContractStorageKey = "scaffoldEth2.selectedContract"; const contractsData = getAllContracts(); const contractNames = Object.keys(contractsData) as ContractName[]; +// let contractNames = [] as ContractName[]; + export function DebugContracts() { + + + + const selectedContractStorageKey = "scaffoldEth2.selectedContract"; + const [selectedContract, setSelectedContract] = useLocalStorage( selectedContractStorageKey, contractNames[0], diff --git a/packages/nextjs/app/debug/_components/contract/ContractReadMethods.tsx b/packages/nextjs/app/debug/_components/contract/ContractReadMethods.tsx index f269fa9..5c1a560 100644 --- a/packages/nextjs/app/debug/_components/contract/ContractReadMethods.tsx +++ b/packages/nextjs/app/debug/_components/contract/ContractReadMethods.tsx @@ -1,27 +1,18 @@ -import { Abi, AbiFunction } from "abitype"; -import { ReadOnlyFunctionForm } from "~~/app/debug/_components/contract"; -import { Contract, ContractName, GenericContract, InheritedFunctions } from "~~/utils/scaffold-eth/contract"; +import { FunctionForm } from "~~/app/debug/_components/contract"; +import { Contract, ContractName } from "~~/utils/scaffold-move/contract"; -export const ContractReadMethods = ({ deployedContractData }: { deployedContractData: Contract }) => { - if (!deployedContractData) { +export const ContractReadMethods = ({ + deployedContractData + }: { + deployedContractData: Contract +}) => { + if (!deployedContractData || deployedContractData.abi === undefined) { return null; } - const functionsToDisplay = ( - ((deployedContractData.abi || []) as Abi).filter(part => part.type === "function") as AbiFunction[] - ) - .filter(fn => { - const isQueryableWithParams = - (fn.stateMutability === "view" || fn.stateMutability === "pure") && fn.inputs.length > 0; - return isQueryableWithParams; - }) - .map(fn => { - return { - fn, - inheritedFrom: ((deployedContractData as GenericContract)?.inheritedFunctions as InheritedFunctions)?.[fn.name], - }; - }) - .sort((a, b) => (b.inheritedFrom ? b.inheritedFrom.localeCompare(a.inheritedFrom) : 1)); + const functionsToDisplay = deployedContractData.abi.exposed_functions.filter((fn) => + fn.is_view, + ); if (!functionsToDisplay.length) { return <>No read methods; @@ -29,15 +20,17 @@ export const ContractReadMethods = ({ deployedContractData }: { deployedContract return ( <> - {functionsToDisplay.map(({ fn, inheritedFrom }) => ( - ( + + ))} */} + - ))} ); }; diff --git a/packages/nextjs/app/debug/_components/contract/ContractUI.tsx b/packages/nextjs/app/debug/_components/contract/ContractUI.tsx index 31fcc7f..029ee30 100644 --- a/packages/nextjs/app/debug/_components/contract/ContractUI.tsx +++ b/packages/nextjs/app/debug/_components/contract/ContractUI.tsx @@ -5,7 +5,8 @@ import { useReducer } from "react"; import { ContractReadMethods } from "./ContractReadMethods"; import { ContractVariables } from "./ContractVariables"; import { ContractWriteMethods } from "./ContractWriteMethods"; -import { Address, Balance } from "~~/components/scaffold-eth"; +import { Address, Balance} from "~~/components/scaffold-move"; + import { useDeployedContractInfo, useNetworkColor } from "~~/hooks/scaffold-eth"; import { useTargetNetwork } from "~~/hooks/scaffold-eth/useTargetNetwork"; import { ContractName } from "~~/utils/scaffold-eth/contract"; @@ -20,7 +21,8 @@ type ContractUIProps = { **/ export const ContractUI = ({ contractName, className = "" }: ContractUIProps) => { const [refreshDisplayVariables, triggerRefreshDisplayVariables] = useReducer(value => !value, false); - const { targetNetwork } = useTargetNetwork(); + // const { targetNetwork } = useTargetNetwork(); + const targetNetwork = "devnet" const { data: deployedContractData, isLoading: deployedContractLoading } = useDeployedContractInfo(contractName); const networkColor = useNetworkColor(); @@ -31,11 +33,10 @@ export const ContractUI = ({ contractName, className = "" }: ContractUIProps) =>
); } - if (!deployedContractData) { return (

- {`No contract found by the name of "${contractName}" on chain "${targetNetwork.name}"!`} + {`No contract found by the name of "${contractName}" on chain "${targetNetwork}"!`}

); } @@ -48,17 +49,18 @@ export const ContractUI = ({ contractName, className = "" }: ContractUIProps) =>
{contractName} -
+ {/*

{deployedContractData.abi.address}

*/} +
Balance: - +
{targetNetwork && (

Network:{" "} - {targetNetwork.name} + {targetNetwork}

)} @@ -74,7 +76,7 @@ export const ContractUI = ({ contractName, className = "" }: ContractUIProps) =>
-

Read

+

View

@@ -86,13 +88,12 @@ export const ContractUI = ({ contractName, className = "" }: ContractUIProps) =>
-

Write

+

Run

diff --git a/packages/nextjs/app/debug/_components/contract/ContractVariables.tsx b/packages/nextjs/app/debug/_components/contract/ContractVariables.tsx index 9d25782..1b7e00e 100644 --- a/packages/nextjs/app/debug/_components/contract/ContractVariables.tsx +++ b/packages/nextjs/app/debug/_components/contract/ContractVariables.tsx @@ -12,39 +12,38 @@ export const ContractVariables = ({ if (!deployedContractData) { return null; } - - const functionsToDisplay = ( - (deployedContractData.abi as Abi).filter(part => part.type === "function") as AbiFunction[] - ) - .filter(fn => { - const isQueryableWithNoParams = - (fn.stateMutability === "view" || fn.stateMutability === "pure") && fn.inputs.length === 0; - return isQueryableWithNoParams; - }) - .map(fn => { - return { - fn, - inheritedFrom: ((deployedContractData as GenericContract)?.inheritedFunctions as InheritedFunctions)?.[fn.name], - }; - }) - .sort((a, b) => (b.inheritedFrom ? b.inheritedFrom.localeCompare(a.inheritedFrom) : 1)); - + // const functionsToDisplay = ( + // (deployedContractData.abi as Abi).filter(part => part.type === "function") as AbiFunction[] + // ) + // .filter(fn => { + // const isQueryableWithNoParams = + // (fn.stateMutability === "view" || fn.stateMutability === "pure") && fn.inputs.length === 0; + // return isQueryableWithNoParams; + // }) + // .map(fn => { + // return { + // fn, + // inheritedFrom: ((deployedContractData as GenericContract)?.inheritedFunctions as InheritedFunctions)?.[fn.name], + // }; + // }) + // .sort((a, b) => (b.inheritedFrom ? b.inheritedFrom.localeCompare(a.inheritedFrom) : 1)); + const functionsToDisplay = [] as AbiFunction[]; if (!functionsToDisplay.length) { return <>No contract variables; } - - return ( - <> - {functionsToDisplay.map(({ fn, inheritedFrom }) => ( - - ))} - - ); + return <>No contract variables; + // return ( + // <> + // {functionsToDisplay.map(({ fn, inheritedFrom }) => ( + // + // ))} + // + // ); }; diff --git a/packages/nextjs/app/debug/_components/contract/ContractWriteMethods.tsx b/packages/nextjs/app/debug/_components/contract/ContractWriteMethods.tsx index ee703a6..ce1b705 100644 --- a/packages/nextjs/app/debug/_components/contract/ContractWriteMethods.tsx +++ b/packages/nextjs/app/debug/_components/contract/ContractWriteMethods.tsx @@ -1,32 +1,18 @@ -import { Abi, AbiFunction } from "abitype"; -import { WriteOnlyFunctionForm } from "~~/app/debug/_components/contract"; -import { Contract, ContractName, GenericContract, InheritedFunctions } from "~~/utils/scaffold-eth/contract"; +import { FunctionForm } from "~~/app/debug/_components/contract"; +import { Contract, ContractName } from "~~/utils/scaffold-move/contract"; export const ContractWriteMethods = ({ - onChange, deployedContractData, }: { - onChange: () => void; deployedContractData: Contract; }) => { - if (!deployedContractData) { + if (!deployedContractData || deployedContractData.abi === undefined) { return null; } - const functionsToDisplay = ( - (deployedContractData.abi as Abi).filter(part => part.type === "function") as AbiFunction[] - ) - .filter(fn => { - const isWriteableFunction = fn.stateMutability !== "view" && fn.stateMutability !== "pure"; - return isWriteableFunction; - }) - .map(fn => { - return { - fn, - inheritedFrom: ((deployedContractData as GenericContract)?.inheritedFunctions as InheritedFunctions)?.[fn.name], - }; - }) - .sort((a, b) => (b.inheritedFrom ? b.inheritedFrom.localeCompare(a.inheritedFrom) : 1)); + const functionsToDisplay = deployedContractData.abi.exposed_functions.filter((fn) => + fn.is_entry, + ); if (!functionsToDisplay.length) { return <>No write methods; @@ -34,14 +20,11 @@ export const ContractWriteMethods = ({ return ( <> - {functionsToDisplay.map(({ fn, inheritedFrom }, idx) => ( - ( + ))} diff --git a/packages/nextjs/app/debug/_components/contract/FunctionForm.tsx b/packages/nextjs/app/debug/_components/contract/FunctionForm.tsx new file mode 100644 index 0000000..3b35af6 --- /dev/null +++ b/packages/nextjs/app/debug/_components/contract/FunctionForm.tsx @@ -0,0 +1,196 @@ +"use client"; + +import {Types} from "aptos"; +import {parseTypeTag} from "@aptos-labs/ts-sdk"; +import { + useWallet, + InputTransactionData, +} from "@aptos-labs/wallet-adapter-react"; + +import { useState } from "react"; +import useSubmitTransaction from "~~/hooks/scaffold-move/useSubmitTransaction"; +import {SubmitHandler} from "react-hook-form"; +import {encodeInputArgsForViewRequest} from "../../../../utils/utils"; +import { view } from "~~/hooks"; + +const zeroInputs = false; + +type ContractFormType = { + typeArgs: string[]; + args: string[]; + ledgerVersion?: string; +}; + +type FunctionFormProps = { + module: Types.MoveModule; + fn: Types.MoveFunction; + write: boolean; +}; + +function removeSignerParam(fn: Types.MoveFunction, write: boolean) { + if (!write) { + return fn.params; + } + return fn.params.filter((p) => p !== "signer" && p !== "&signer"); +} + +export const FunctionForm = ({ + module, + fn, + write, +}: FunctionFormProps) => { + const {submitTransaction, transactionResponse, transactionInProcess} = useSubmitTransaction(); + const [inProcess, setInProcess] = useState(false); + const [result, setResult] = useState(); + const [data, setData] = useState({ typeArgs: [], args: [] }); + + const fnParams = removeSignerParam(fn, write); + + const convertArgument = (arg: string | null | undefined, type: string): any => { + if (typeof arg !== "string") { + arg = ""; + } + arg = arg.trim(); + const typeTag = parseTypeTag(type); + if (typeTag.isVector()) { + const innerTag = typeTag.value; + if (innerTag.isVector()) { + return JSON.parse(arg) as any[]; + } + if (innerTag.isU8()) { + if (arg.startsWith("0x")) { + return arg; + } + } + if (arg.startsWith("[")) { + return JSON.parse(arg) as any[]; + } else { + return arg.split(",").map((arg) => { + return arg.trim(); + }); + } + } else if (typeTag.isStruct()) { + if (typeTag.isOption()) { + if (arg === "") { + return undefined; + } else { + arg = convertArgument(arg, typeTag.value.typeArgs[0].toString()); + return arg; + } + } + } + return arg; + }; + + const handleWrite = async () => { + const payload: InputTransactionData = { + data: { + function: `${module.address}::${module.name}::${fn.name}`, + typeArguments: data.typeArgs, + functionArguments: data.args.map((arg, i) => { + const type = fnParams[i]; + return convertArgument(arg, type); + }), + }, + }; + + try { + await submitTransaction(payload); + + if (transactionResponse?.transactionSubmitted) { + console.log("function_interacted", fn.name, { + txn_status: transactionResponse.success ? "success" : "failed", + }); + } + } catch (e: any) { + console.error("โšก๏ธ ~ file: FunctionForm.tsx:handleWrite ~ error", e); + } + }; + + const handleView = async () => { + let viewRequest: Types.ViewRequest; + try { + viewRequest = { + function: `${module.address}::${module.name}::${fn.name}`, + type_arguments: data.typeArgs, + arguments: data.args.map((arg, i) => { + return encodeInputArgsForViewRequest(fn.params[i], arg); + }), + }; + } catch (e: any) { + console.error("Parsing arguments failed: " + e?.message); + return; + } + setInProcess(true); + try { + const result = await view(viewRequest, state.network_value, data.ledgerVersion); + setResult(result); + console.log("function_interacted", fn.name, { txn_status: "success" }); + } catch (e: any) { + let error = e.message ?? JSON.stringify(e); + const prefix = "Error:"; + if (error.startsWith(prefix)) { + error = error.substring(prefix.length).trim(); + } + setResult(undefined); + console.log("function_interacted", fn.name, { txn_status: "failed" }); + } + setInProcess(false); + }; + + const isFunctionSuccess = !!( + transactionResponse?.transactionSubmitted && transactionResponse?.success + ); + + return ( +
+
+

+ {fn.name} +

+ {fnParams.map((param, i) => { + const isOption = param.startsWith("0x1::option::Option"); + return ( +
+
+ {`arg${i}:`} +
+
+ { + const newArgs = [...data.args]; + newArgs[i] = e.target.value; + setData({ ...data, args: newArgs }); + }} + /> +
+
+ ); + })} +
+ {!zeroInputs && ( +
+ )} + {write && ( + + )} + {!write && ( + + )} +
+
+ {transactionResponse ? ( +
+ {transactionResponse.message} +
+ ) : null} +
+ ); +}; diff --git a/packages/nextjs/app/debug/_components/contract/ReadOnlyFunctionForm.tsx b/packages/nextjs/app/debug/_components/contract/ReadOnlyFunctionForm.tsx deleted file mode 100644 index a0d097a..0000000 --- a/packages/nextjs/app/debug/_components/contract/ReadOnlyFunctionForm.tsx +++ /dev/null @@ -1,102 +0,0 @@ -"use client"; - -import { useEffect, useState } from "react"; -import { InheritanceTooltip } from "./InheritanceTooltip"; -import { Abi, AbiFunction } from "abitype"; -import { Address } from "viem"; -import { useReadContract } from "wagmi"; -import { - ContractInput, - displayTxResult, - getFunctionInputKey, - getInitialFormState, - getParsedContractFunctionArgs, - transformAbiFunction, -} from "~~/app/debug/_components/contract"; -import { useTargetNetwork } from "~~/hooks/scaffold-eth/useTargetNetwork"; -import { getParsedError, notification } from "~~/utils/scaffold-eth"; - -type ReadOnlyFunctionFormProps = { - contractAddress: Address; - abiFunction: AbiFunction; - inheritedFrom?: string; - abi: Abi; -}; - -export const ReadOnlyFunctionForm = ({ - contractAddress, - abiFunction, - inheritedFrom, - abi, -}: ReadOnlyFunctionFormProps) => { - const [form, setForm] = useState>(() => getInitialFormState(abiFunction)); - const [result, setResult] = useState(); - const { targetNetwork } = useTargetNetwork(); - - const { isFetching, refetch, error } = useReadContract({ - address: contractAddress, - functionName: abiFunction.name, - abi: abi, - args: getParsedContractFunctionArgs(form), - chainId: targetNetwork.id, - query: { - enabled: false, - retry: false, - }, - }); - - useEffect(() => { - if (error) { - const parsedError = getParsedError(error); - notification.error(parsedError); - } - }, [error]); - - const transformedFunction = transformAbiFunction(abiFunction); - const inputElements = transformedFunction.inputs.map((input, inputIndex) => { - const key = getFunctionInputKey(abiFunction.name, input, inputIndex); - return ( - { - setResult(undefined); - setForm(updatedFormValue); - }} - form={form} - stateObjectKey={key} - paramType={input} - /> - ); - }); - - return ( -
-

- {abiFunction.name} - -

- {inputElements} -
-
- {result !== null && result !== undefined && ( -
-

Result:

-
{displayTxResult(result, "sm")}
-
- )} -
- -
-
- ); -}; diff --git a/packages/nextjs/app/debug/_components/contract/WriteOnlyFunctionForm.tsx b/packages/nextjs/app/debug/_components/contract/WriteOnlyFunctionForm.tsx deleted file mode 100644 index b8e8f84..0000000 --- a/packages/nextjs/app/debug/_components/contract/WriteOnlyFunctionForm.tsx +++ /dev/null @@ -1,141 +0,0 @@ -"use client"; - -import { useEffect, useState } from "react"; -import { InheritanceTooltip } from "./InheritanceTooltip"; -import { Abi, AbiFunction } from "abitype"; -import { Address, TransactionReceipt } from "viem"; -import { useAccount, useWaitForTransactionReceipt, useWriteContract } from "wagmi"; -import { - ContractInput, - TxReceipt, - getFunctionInputKey, - getInitialFormState, - getParsedContractFunctionArgs, - transformAbiFunction, -} from "~~/app/debug/_components/contract"; -import { IntegerInput } from "~~/components/scaffold-eth"; -import { useTransactor } from "~~/hooks/scaffold-eth"; -import { useTargetNetwork } from "~~/hooks/scaffold-eth/useTargetNetwork"; - -type WriteOnlyFunctionFormProps = { - abi: Abi; - abiFunction: AbiFunction; - onChange: () => void; - contractAddress: Address; - inheritedFrom?: string; -}; - -export const WriteOnlyFunctionForm = ({ - abi, - abiFunction, - onChange, - contractAddress, - inheritedFrom, -}: WriteOnlyFunctionFormProps) => { - const [form, setForm] = useState>(() => getInitialFormState(abiFunction)); - const [txValue, setTxValue] = useState(""); - const { chain } = useAccount(); - const writeTxn = useTransactor(); - const { targetNetwork } = useTargetNetwork(); - const writeDisabled = !chain || chain?.id !== targetNetwork.id; - - const { data: result, isPending, writeContractAsync } = useWriteContract(); - - const handleWrite = async () => { - if (writeContractAsync) { - try { - const makeWriteWithParams = () => - writeContractAsync({ - address: contractAddress, - functionName: abiFunction.name, - abi: abi, - args: getParsedContractFunctionArgs(form), - value: BigInt(txValue), - }); - await writeTxn(makeWriteWithParams); - onChange(); - } catch (e: any) { - console.error("โšก๏ธ ~ file: WriteOnlyFunctionForm.tsx:handleWrite ~ error", e); - } - } - }; - - const [displayedTxResult, setDisplayedTxResult] = useState(); - const { data: txResult } = useWaitForTransactionReceipt({ - hash: result, - }); - useEffect(() => { - setDisplayedTxResult(txResult); - }, [txResult]); - - // TODO use `useMemo` to optimize also update in ReadOnlyFunctionForm - const transformedFunction = transformAbiFunction(abiFunction); - const inputs = transformedFunction.inputs.map((input, inputIndex) => { - const key = getFunctionInputKey(abiFunction.name, input, inputIndex); - return ( - { - setDisplayedTxResult(undefined); - setForm(updatedFormValue); - }} - form={form} - stateObjectKey={key} - paramType={input} - /> - ); - }); - const zeroInputs = inputs.length === 0 && abiFunction.stateMutability !== "payable"; - - return ( -
-
-

- {abiFunction.name} - -

- {inputs} - {abiFunction.stateMutability === "payable" ? ( -
-
- payable value - wei -
- { - setDisplayedTxResult(undefined); - setTxValue(updatedTxValue); - }} - placeholder="value (wei)" - /> -
- ) : null} -
- {!zeroInputs && ( -
- {displayedTxResult ? : null} -
- )} -
- -
-
-
- {zeroInputs && txResult ? ( -
- -
- ) : null} -
- ); -}; diff --git a/packages/nextjs/app/debug/_components/contract/index.tsx b/packages/nextjs/app/debug/_components/contract/index.tsx index 83833d8..059863a 100644 --- a/packages/nextjs/app/debug/_components/contract/index.tsx +++ b/packages/nextjs/app/debug/_components/contract/index.tsx @@ -1,8 +1,7 @@ export * from "./ContractInput"; export * from "./ContractUI"; export * from "./DisplayVariable"; -export * from "./ReadOnlyFunctionForm"; export * from "./TxReceipt"; export * from "./utilsContract"; export * from "./utilsDisplay"; -export * from "./WriteOnlyFunctionForm"; +export * from "./FunctionForm" diff --git a/packages/nextjs/app/debug/page.tsx b/packages/nextjs/app/debug/page.tsx index e6fb89f..267b3c6 100644 --- a/packages/nextjs/app/debug/page.tsx +++ b/packages/nextjs/app/debug/page.tsx @@ -4,7 +4,7 @@ import { getMetadata } from "~~/utils/scaffold-eth/getMetadata"; export const metadata = getMetadata({ title: "Debug Contracts", - description: "Debug your deployed ๐Ÿ— Scaffold-ETH 2 contracts in an easy way", + description: "Debug your deployed ๐Ÿ— Scaffold-Move contracts in an easy way", }); const Debug: NextPage = () => { diff --git a/packages/nextjs/app/page.tsx b/packages/nextjs/app/page.tsx index b91c22f..c9becca 100644 --- a/packages/nextjs/app/page.tsx +++ b/packages/nextjs/app/page.tsx @@ -15,7 +15,7 @@ const Home: NextPage = () => {

Welcome to - Scaffold-ETH 2 + Scaffold-Move

Connected Address:

@@ -30,11 +30,11 @@ const Home: NextPage = () => {

Edit your smart contract{" "} - YourContract.sol + OnchainBio.move {" "} in{" "} - packages/hardhat/contracts + packages/move/sources

diff --git a/packages/nextjs/components/Header.tsx b/packages/nextjs/components/Header.tsx index 6d4a03d..fde57e5 100644 --- a/packages/nextjs/components/Header.tsx +++ b/packages/nextjs/components/Header.tsx @@ -20,6 +20,10 @@ export const menuLinks: HeaderMenuLink[] = [ label: "Home", href: "/", }, + { + label: "Bio", + href: "/bio", + }, { label: "Debug Contracts", href: "/debug", @@ -94,8 +98,8 @@ export const Header = () => { SE2 logo
- Scaffold-ETH - Ethereum dev stack + Scaffold-Move + Move dev stack
    diff --git a/packages/nextjs/components/scaffold-move/Address.tsx b/packages/nextjs/components/scaffold-move/Address.tsx new file mode 100644 index 0000000..24101ab --- /dev/null +++ b/packages/nextjs/components/scaffold-move/Address.tsx @@ -0,0 +1,134 @@ +"use client"; + +import { useState } from "react"; +import Link from "next/link"; +import { CopyToClipboard } from "react-copy-to-clipboard"; +import { hardhat } from "viem/chains"; +import { CheckCircleIcon, DocumentDuplicateIcon } from "@heroicons/react/24/outline"; +import { BlockieAvatar } from "~~/components/scaffold-eth"; +import { useTargetNetwork } from "~~/hooks/scaffold-eth/useTargetNetwork"; +import { getBlockExplorerAddressLink } from "~~/utils/scaffold-eth"; + +type AddressProps = { + address?: string; + disableAddressLink?: boolean; + format?: "short" | "long"; + size?: "xs" | "sm" | "base" | "lg" | "xl" | "2xl" | "3xl"; +}; + +const blockieSizeMap = { + xs: 6, + sm: 7, + base: 8, + lg: 9, + xl: 10, + "2xl": 12, + "3xl": 15, +}; + +/** + * Displays an address (or ENS) with a Blockie image and option to copy address. + */ +export const Address = ({ address, disableAddressLink, format, size = "base" }: AddressProps) => { + const [ens, setEns] = useState(); + const [ensAvatar, setEnsAvatar] = useState(); + const [addressCopied, setAddressCopied] = useState(false); + + const { targetNetwork } = useTargetNetwork(); + + // const { data: fetchedEns } = useEnsName({ + // address: address, + // chainId: 1, + // query: { + // enabled: isAddress(address ?? ""), + // }, + // }); + // const { data: fetchedEnsAvatar } = useEnsAvatar({ + // name: fetchedEns ? normalize(fetchedEns) : undefined, + // chainId: 1, + // query: { + // enabled: Boolean(fetchedEns), + // gcTime: 30_000, + // }, + // }); + + // We need to apply this pattern to avoid Hydration errors. + // useEffect(() => { + // setEns(fetchedEns); + // }, [fetchedEns]); + + // useEffect(() => { + // setEnsAvatar(fetchedEnsAvatar); + // }, [fetchedEnsAvatar]); + + // Skeleton UI + if (!address) { + return ( +
    +
    +
    +
    +
    +
    + ); + } + + + const blockExplorerAddressLink = getBlockExplorerAddressLink(targetNetwork, address); + let displayAddress = address?.slice(0, 6) + "..." + address?.slice(-4); + + if (ens) { + displayAddress = ens; + } else if (format === "long") { + displayAddress = address; + } + + return ( +
    +
    + +
    + {disableAddressLink ? ( + {displayAddress} + ) : targetNetwork.id === hardhat.id ? ( + + {displayAddress} + + ) : ( + + {displayAddress} + + )} + {addressCopied ? ( +
    + ); +}; diff --git a/packages/nextjs/components/scaffold-move/Balance.tsx b/packages/nextjs/components/scaffold-move/Balance.tsx new file mode 100644 index 0000000..e9a7064 --- /dev/null +++ b/packages/nextjs/components/scaffold-move/Balance.tsx @@ -0,0 +1,47 @@ +"use client"; + +import { Address, formatEther } from "viem"; +import { useTargetNetwork } from "~~/hooks/scaffold-eth/useTargetNetwork"; +import { useWatchBalance } from "~~/hooks/scaffold-eth/useWatchBalance"; +import {useGetAccountAPTBalance} from "~~/hooks/scaffold-move/useGetAccountAPTBalance"; +import {getFormattedBalanceStr} from "../../utils/scaffold-move/ContentValue/CurrencyValue" + +type BalanceProps = { + address: string; +}; + +/** + * Display APT balance of an APT address. + */ +export const Balance = ({ address }: BalanceProps) => { + const balance = useGetAccountAPTBalance(address); + + if (!address || balance === null) { + return ( +
    +
    +
    +
    +
    +
    + ); + } + + // if (isError) { + // return ( + //
    + //
    Error
    + //
    + // ); + // } + // const formattedBalance = balance ? Number(formatEther(balance.value)) : 0; + + return ( +
    + <> + {getFormattedBalanceStr(balance)} + MOVE + +
    + ); +}; diff --git a/packages/nextjs/components/scaffold-move/index.tsx b/packages/nextjs/components/scaffold-move/index.tsx new file mode 100644 index 0000000..c64f173 --- /dev/null +++ b/packages/nextjs/components/scaffold-move/index.tsx @@ -0,0 +1,2 @@ +export * from "./Address"; +export * from "./Balance"; \ No newline at end of file diff --git a/packages/nextjs/constants.tsx b/packages/nextjs/constants.tsx new file mode 100644 index 0000000..432a365 --- /dev/null +++ b/packages/nextjs/constants.tsx @@ -0,0 +1,87 @@ +/** + * Network + */ +export const devnetUrl = "https://aptos.devnet.m1.movementlabs.xyz"; + +export const networks = { + mainnet: "https://aptos.movementlabs.xyz", + testnet: "https://aptos.testnet.movementlabs.xyz", + devnet: devnetUrl, + local: "http://127.0.0.1:8080/v1", + previewnet: "https://aptos.testnet.movementlabs.xyz", + randomnet: "https://aptos.testnet.movementlabs.xyz", +}; + +export type NetworkName = keyof typeof networks; + +export function isValidNetworkName(value: string): value is NetworkName { + return value in networks; +} + +export enum Network { + MAINNET = "mainnet", + TESTNET = "testnet", + DEVNET = "devnet", + LOCAL = "local", + PREVIEWNET = "previewnet", + RANDOMNET = "randomnet", +} + +// Remove trailing slashes +for (const key of Object.keys(networks)) { + const networkName = key as NetworkName; + if (networks[networkName].endsWith("/")) { + networks[networkName] = networks[networkName].slice(0, -1); + } +} + +export const defaultNetworkName: NetworkName = "devnet" as const; + +if (!(defaultNetworkName in networks)) { + throw `defaultNetworkName '${defaultNetworkName}' not in Networks!`; +} + +export const defaultNetwork = networks[defaultNetworkName]; + +/** + * Feature + */ +export const features = { + prod: "Production Mode", + dev: "Development Mode", + earlydev: "Early Development Mode", +}; + +export type FeatureName = keyof typeof features; +export function isValidFeatureName(value: string): value is FeatureName { + return value in features; +} + +// Remove trailing slashes +for (const key of Object.keys(features)) { + const featureName = key as FeatureName; + if (features[featureName].endsWith("/")) { + features[featureName] = features[featureName].slice(0, -1); + } +} + +export const defaultFeatureName: FeatureName = "prod" as const; + +if (!(defaultFeatureName in features)) { + throw `defaultFeatureName '${defaultFeatureName}' not in Features!`; +} + +export const defaultFeature = features[defaultFeatureName]; + +/** + * Delegation Service + */ +export const OCTA = 100000000; +export const WHILTELISTED_TESTNET_DELEGATION_NODES = null; + +/** + * Core Address + */ +export const objectCoreAddress = "0x1::object::ObjectCore"; +export const tokenV2Address = "0x4::token::Token"; +export const collectionV2Address = "0x4::collection::Collection"; diff --git a/packages/nextjs/contracts/deployedContracts.ts b/packages/nextjs/contracts/deployedContracts.ts deleted file mode 100644 index 008d4eb..0000000 --- a/packages/nextjs/contracts/deployedContracts.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * This file is autogenerated by Scaffold-ETH. - * You should not edit it manually or your changes might be overwritten. - */ -import { GenericContractsDeclaration } from "~~/utils/scaffold-eth/contract"; - -const deployedContracts = {} as const; - -export default deployedContracts satisfies GenericContractsDeclaration; diff --git a/packages/nextjs/contracts/deployedModules.ts b/packages/nextjs/contracts/deployedModules.ts new file mode 100644 index 0000000..6396ffa --- /dev/null +++ b/packages/nextjs/contracts/deployedModules.ts @@ -0,0 +1,20 @@ +import { GenericContractsDeclaration } from "~~/utils/scaffold-move/contract"; + +const deployedContracts = { + "devnet": { + "onchain_bio": { + "bytecode": "0xa11ceb0b060000000a010006020608030e0f051d1307304a087a4010ba01220adc01090ce501340d99020200000101010200030e0002040700000500010000060203000109050000010501080103060c080108010001080001060c0b6f6e636861696e5f62696f067369676e657206737472696e670342696f06537472696e67076765745f62696f087265676973746572046e616d650362696f0a616464726573735f6f664295cca96321b2807473c0df06fa0ec4b1e22e612f8577cc36406d8c0e67630c0000000000000000000000000000000000000000000000000000000000000001126170746f733a3a6d657461646174615f76310e000001076765745f62696f010100000202070801080801000100010003050b002b0010001402010104010004100a001102290004080a0011022c00010b010b0212000c030b000b032d0002000100", + "abi": {"address":"0x4295cca96321b2807473c0df06fa0ec4b1e22e612f8577cc36406d8c0e67630c","name":"onchain_bio","friends":[],"exposed_functions":[{"name":"get_bio","visibility":"public","is_entry":false,"is_view":true,"generic_type_params":[],"params":["address"],"return":["0x1::string::String"]},{"name":"register","visibility":"public","is_entry":true,"is_view":false,"generic_type_params":[],"params":["&signer","0x1::string::String","0x1::string::String"],"return":[]}],"structs":[{"name":"Bio","is_native":false,"abilities":["drop","store","key"],"generic_type_params":[],"fields":[{"name":"name","type":"0x1::string::String"},{"name":"bio","type":"0x1::string::String"}]}]} + }, +"onchain_poems": { + "bytecode": "0xa11ceb0b060000000b010004020408030c0a05161207284c08744006b4010a10be013a0af8010c0c8402260daa02020000010100020e000103070000040001000005020300010501080104060c080108010801000108000d6f6e636861696e5f706f656d7306737472696e670b496e736372697074696f6e06537472696e67086765745f706f656d08726567697374657204706f656d057469746c6506617574686f724295cca96321b2807473c0df06fa0ec4b1e22e612f8577cc36406d8c0e67630c000000000000000000000000000000000000000000000000000000000000000103080100000000000000126170746f733a3a6d657461646174615f7631260101000000000000000d455f414c52454144595f484153000001086765745f706f656d010100000203060801070801080801000100010003050b002b00100014020101040004090b010b020b0312000c040b000b042d0002000000", + "abi": {"address":"0x4295cca96321b2807473c0df06fa0ec4b1e22e612f8577cc36406d8c0e67630c","name":"onchain_poems","friends":[],"exposed_functions":[{"name":"get_poem","visibility":"public","is_entry":false,"is_view":true,"generic_type_params":[],"params":["address"],"return":["0x1::string::String"]},{"name":"register","visibility":"public","is_entry":true,"is_view":false,"generic_type_params":[],"params":["&signer","0x1::string::String","0x1::string::String","0x1::string::String"],"return":[]}],"structs":[{"name":"Inscription","is_native":false,"abilities":["drop","store","key"],"generic_type_params":[],"fields":[{"name":"poem","type":"0x1::string::String"},{"name":"title","type":"0x1::string::String"},{"name":"author","type":"0x1::string::String"}]}]} + }, +"onchain_poems_with_table": { + "bytecode": "0xa11ceb0b060000000c01000a020a14031e23044106054739078001bb0108bb024006fb020a1085033a0abf031c0cdb036f0dca0404000001010102010301040005070000060800030707000410040203010001000800010000090201000212020400041306010203020114080101060415010a02030403050407050504060c0802080208020001060c040308000708010501050203080003070b03020900090109000901010800010900010801010b030209000901186f6e636861696e5f706f656d735f776974685f7461626c65056576656e74067369676e657206737472696e67057461626c6504506f656d08506f656d4c69737406537472696e670b6372656174655f706f656d106372656174655f706f656d5f6c69737407706f656d5f6964076164647265737304706f656d057469746c6506617574686f7205706f656d73055461626c650c706f656d5f636f756e7465720a616464726573735f6f660675707365727404656d6974036e65774295cca96321b2807473c0df06fa0ec4b1e22e612f8577cc36406d8c0e67630c000000000000000000000000000000000000000000000000000000000000000103080100000000000000126170746f733a3a6d657461646174615f76312601010000000000000011455f4e4f545f494e495449414c495a4544000104506f656d010400000002050a030b050c08020d08020e08020102020f0b03020308001103000104010103250b0011020c070a072901040705090700270a072a010c060a06100014060100000000000000160c040a040b070b010b020b0312000c050a060f010a040a0538000b040b060f00150b05380102010104000908380206000000000000000012010c010b000b012d01020101010000", + "abi": {"address":"0x4295cca96321b2807473c0df06fa0ec4b1e22e612f8577cc36406d8c0e67630c","name":"onchain_poems_with_table","friends":[],"exposed_functions":[{"name":"create_poem","visibility":"public","is_entry":true,"is_view":false,"generic_type_params":[],"params":["&signer","0x1::string::String","0x1::string::String","0x1::string::String"],"return":[]},{"name":"create_poem_list","visibility":"public","is_entry":true,"is_view":false,"generic_type_params":[],"params":["&signer"],"return":[]}],"structs":[{"name":"Poem","is_native":false,"abilities":["copy","drop","store"],"generic_type_params":[],"fields":[{"name":"poem_id","type":"u64"},{"name":"address","type":"address"},{"name":"poem","type":"0x1::string::String"},{"name":"title","type":"0x1::string::String"},{"name":"author","type":"0x1::string::String"}]},{"name":"PoemList","is_native":false,"abilities":["key"],"generic_type_params":[],"fields":[{"name":"poems","type":"0x1::table::Table"},{"name":"poem_counter","type":"u64"}]}]} + } + } +} as const; + +export default deployedContracts satisfies GenericContractsDeclaration; \ No newline at end of file diff --git a/packages/nextjs/contracts/externalContracts.ts b/packages/nextjs/contracts/externalContracts.ts deleted file mode 100644 index ab6daa8..0000000 --- a/packages/nextjs/contracts/externalContracts.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { GenericContractsDeclaration } from "~~/utils/scaffold-eth/contract"; - -/** - * @example - * const externalContracts = { - * 1: { - * DAI: { - * address: "0x...", - * abi: [...], - * }, - * }, - * } as const; - */ -const externalContracts = {} as const; - -export default externalContracts satisfies GenericContractsDeclaration; diff --git a/packages/nextjs/contracts/externalModules.ts b/packages/nextjs/contracts/externalModules.ts new file mode 100644 index 0000000..68eed0b --- /dev/null +++ b/packages/nextjs/contracts/externalModules.ts @@ -0,0 +1,9 @@ +import { GenericContractsDeclaration } from "~~/utils/scaffold-move/contract"; + +const externalContracts = { + "devnet": { + + } +} as const; + +export default externalContracts satisfies GenericContractsDeclaration; \ No newline at end of file diff --git a/packages/nextjs/global-config/GlobalConfig.tsx b/packages/nextjs/global-config/GlobalConfig.tsx new file mode 100644 index 0000000..727037d --- /dev/null +++ b/packages/nextjs/global-config/GlobalConfig.tsx @@ -0,0 +1,117 @@ +import {AptosClient, IndexerClient} from "aptos"; +import React, {useMemo} from "react"; +import { + FeatureName, + NetworkName, + defaultNetworkName, + networks, +} from "../constants"; +import { + getSelectedFeatureFromLocalStorage, + useFeatureSelector, +} from "./feature-selection"; +import {useNetworkSelector} from "./network-selection"; +import {getGraphqlURI} from "../hooks/scaffold-move/useGraphqlClient"; +import {Aptos, AptosConfig, NetworkToNetworkName} from "@aptos-labs/ts-sdk"; + +const HEADERS = { + "x-indexer-client": "aptos-explorer", +}; + +export type GlobalState = { + /** actual state */ + readonly feature_name: FeatureName; + /** derived from external state ?network= query parameter - e.g. devnet */ + readonly network_name: NetworkName; + /** derived from network_name - url to connect to network */ + readonly network_value: string; + /** derived from network_value */ + readonly aptos_client: AptosClient; + /** derived from network_value */ + readonly indexer_client?: IndexerClient; + /** derived from network_value */ + readonly sdk_v2_client?: Aptos; +}; + +type GlobalActions = { + selectFeature: ReturnType[1]; + selectNetwork: ReturnType[1]; +}; + +function deriveGlobalState({ + feature_name, + network_name, +}: { + feature_name: FeatureName; + network_name: NetworkName; +}): GlobalState { + const indexerUri = getGraphqlURI(network_name); + let indexerClient = undefined; + if (indexerUri) { + indexerClient = new IndexerClient(indexerUri, {HEADERS}); + } + return { + feature_name, + network_name, + network_value: networks[network_name], + aptos_client: new AptosClient(networks[network_name], { + HEADERS, + }), + indexer_client: indexerClient, + sdk_v2_client: new Aptos( + new AptosConfig({ + network: NetworkToNetworkName[network_name], + clientConfig: { + HEADERS, + }, + }), + ), + }; +} + +const initialGlobalState = deriveGlobalState({ + feature_name: getSelectedFeatureFromLocalStorage(), + network_name: defaultNetworkName, +}); + +export const GlobalStateContext = React.createContext(initialGlobalState); +export const GlobalActionsContext = React.createContext({} as GlobalActions); + +export const GlobalStateProvider = ({ + children, +}: { + children: React.ReactNode; +}) => { + const [selectedFeature, selectFeature] = useFeatureSelector(); + const [selectedNetwork, selectNetwork] = useNetworkSelector(); + const globalState: GlobalState = useMemo( + () => + deriveGlobalState({ + feature_name: selectedFeature, + network_name: selectedNetwork, + }), + [selectedFeature, selectedNetwork], + ); + + const globalActions = useMemo( + () => ({ + selectFeature, + selectNetwork, + }), + [selectFeature, selectNetwork], + ); + + return ( + + + {children} + + + ); +}; + +export const useGlobalState = () => + [ + React.useContext(GlobalStateContext), + React.useContext(GlobalActionsContext), + ] as const; diff --git a/packages/nextjs/global-config/feature-selection.ts b/packages/nextjs/global-config/feature-selection.ts new file mode 100644 index 0000000..9241779 --- /dev/null +++ b/packages/nextjs/global-config/feature-selection.ts @@ -0,0 +1,63 @@ +import {useSearchParams} from "react-router-dom"; +import { + FeatureName, + defaultFeatureName, + features, + isValidFeatureName, +} from "../constants"; +import {useCallback, useEffect, useState} from "react"; + +export function getSelectedFeatureFromLocalStorage(): FeatureName { + let selected_feature = localStorage.getItem("selected_feature"); + if (selected_feature) { + selected_feature = selected_feature.toLowerCase(); + if (selected_feature in features) { + return selected_feature as FeatureName; + } + } + return defaultFeatureName; +} + +// This is a custom hook that allows us to select a feature +// The feature is stored in local storage across sessions and also in the url as a query param during the session lifetime. +// don't use this hook directly in components, rather use: const [useGlobalState, {selectFeature}] = useGlobalState(); +export function useFeatureSelector() { + const [searchParams, setSearchParams] = useSearchParams(); + const [selectedFeature, setSelectedFeature] = useState( + getSelectedFeatureFromLocalStorage, + ); + + const featureQueryParam = searchParams.get("feature"); + + const selectFeature = useCallback( + (feature_name: FeatureName) => { + if (!isValidFeatureName(feature_name)) return; + localStorage.setItem("selected_feature", feature_name); + // only show the "feature" param in the url when it's not "prod" + // we don't want the users to know the existence of the "feature" param + if (feature_name === defaultFeatureName) { + setSearchParams((prev) => { + const newParams = new URLSearchParams(prev); + newParams.delete("feature"); + return newParams; + }); + } else { + setSearchParams((prev) => { + const newParams = new URLSearchParams(prev); + newParams.set("feature", feature_name); + return newParams; + }); + } + setSelectedFeature(feature_name); + }, + [setSearchParams], + ); + + useEffect(() => { + if (featureQueryParam) { + selectFeature(featureQueryParam as FeatureName); + } + }, [featureQueryParam, selectFeature]); + + return [selectedFeature, selectFeature] as const; +} diff --git a/packages/nextjs/global-config/network-selection.ts b/packages/nextjs/global-config/network-selection.ts new file mode 100644 index 0000000..cf1a6ec --- /dev/null +++ b/packages/nextjs/global-config/network-selection.ts @@ -0,0 +1,80 @@ +import {useSearchParams} from "react-router-dom"; +import { + NetworkName, + isValidNetworkName, + defaultNetworkName, +} from "../constants"; +import {useEffect} from "react"; + +const SELECTED_NETWORK_LOCAL_STORAGE_KEY = "selected_network"; + +function getUserSelectedNetworkFromLocalStorageWithDefault(): NetworkName { + const network = localStorage.getItem(SELECTED_NETWORK_LOCAL_STORAGE_KEY); + if (!isValidNetworkName(network ?? "")) { + return defaultNetworkName; + } + return network as NetworkName; +} + +function writeSelectedNetworkToLocalStorage(network: NetworkName) { + const currentLocalStorageNetwork = localStorage.getItem( + SELECTED_NETWORK_LOCAL_STORAGE_KEY, + ); + if (network === defaultNetworkName && currentLocalStorageNetwork != null) { + // if network selection is default network (i.e. mainnet) we remove the local storage entry + localStorage.removeItem(SELECTED_NETWORK_LOCAL_STORAGE_KEY); + } else if (currentLocalStorageNetwork !== network) { + localStorage.setItem(SELECTED_NETWORK_LOCAL_STORAGE_KEY, network); + } +} + +// This is a custom hook that allows us to read and write the selectedNetwork. +// Note that this hook implements essentially 3 things: +// 1. The hook will return the currently selected network, which is essentially whatever is contained in the URL query param "network". +// 2. If the URL query param "network" is not present, the hook will perform this on initialization: +// 1. check localStorage for a previously selected network. If no previously selected network is found, it will default to the defaultNetworkName. +// 2. set the URL query param "network" to the result of 1. +// 3. Lastly, the hook provides a function to explicitly select/switch a network. This function will update the URL query param "network" and also store the selected network in local storage. +// This is aimed to be used by the network selection dropdown in the header. +// WARNING: don't use this hook directly in components, rather use: const [useGlobalState, {selectNetwork}] = useGlobalState(); +export function useNetworkSelector() { + const [searchParams, setSearchParams] = useSearchParams(); + + const selectedNetworkQueryParam = searchParams.get("network") ?? ""; + + function selectNetwork( + network: NetworkName, + {replace = false}: {replace?: boolean} = {}, + ) { + if (!isValidNetworkName(network)) return; + setSearchParams( + (prev) => { + const newParams = new URLSearchParams(prev); + newParams.set("network", network); + return newParams; + }, + {replace}, + ); + writeSelectedNetworkToLocalStorage(network); + } + + // on init check for existence of network query param, if not present, check local storage for a previously selected network. Then set query param to the network defined in local storage. + useEffect( + () => { + const currentNetworkSearchParam = searchParams.get("network"); + if (!isValidNetworkName(currentNetworkSearchParam ?? "")) { + selectNetwork(getUserSelectedNetworkFromLocalStorageWithDefault(), { + replace: true, + }); + } + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [], // empty [] makes this effect only run once (on mount) + ); + + if (isValidNetworkName(selectedNetworkQueryParam)) { + return [selectedNetworkQueryParam, selectNetwork] as const; + } else { + return [defaultNetworkName, selectNetwork] as const; + } +} diff --git a/packages/nextjs/hooks/client.ts b/packages/nextjs/hooks/client.ts new file mode 100644 index 0000000..b199628 --- /dev/null +++ b/packages/nextjs/hooks/client.ts @@ -0,0 +1,37 @@ +export enum ResponseErrorType { + NOT_FOUND = "Not found", + UNHANDLED = "Unhandled", + TOO_MANY_REQUESTS = "To Many Requests", +} + +export type ResponseError = + | {type: ResponseErrorType.NOT_FOUND; message?: string} + | {type: ResponseErrorType.UNHANDLED; message: string} + | {type: ResponseErrorType.TOO_MANY_REQUESTS; message?: string}; + +export async function withResponseError(promise: Promise): Promise { + return await promise.catch((error) => { + console.error("ERROR!", error, typeof error); + if (typeof error == "object" && "status" in error) { + // This is a request! + error = error as Response; + if (error.status === 404) { + throw {type: ResponseErrorType.NOT_FOUND}; + } + } + if ( + error.message + .toLowerCase() + .includes(ResponseErrorType.TOO_MANY_REQUESTS.toLowerCase()) + ) { + throw { + type: ResponseErrorType.TOO_MANY_REQUESTS, + }; + } + + throw { + type: ResponseErrorType.UNHANDLED, + message: error.toString(), + }; + }); +} diff --git a/packages/nextjs/hooks/index.ts b/packages/nextjs/hooks/index.ts new file mode 100644 index 0000000..502852e --- /dev/null +++ b/packages/nextjs/hooks/index.ts @@ -0,0 +1,313 @@ +import {AptosClient, Types} from "aptos"; +import {withResponseError} from "./client"; + +// export async function getTransactions( +// requestParameters: {start?: number; limit?: number}, +// nodeUrl: string, +// ): Promise { +// const client = new AptosClient(nodeUrl); +// const {start, limit} = requestParameters; +// let bigStart; +// if (start !== undefined) { +// bigStart = BigInt(start); +// } +// const transactions = await withResponseError( +// client.getTransactions({start: bigStart, limit}), +// ); + +// // Sort in descending order +// transactions.sort(sortTransactions); + +// return transactions; +// } + +// export async function getAccountTransactions( +// requestParameters: {address: string; start?: number; limit?: number}, +// nodeUrl: string, +// ): Promise { +// const client = new AptosClient(nodeUrl); +// const {address, start, limit} = requestParameters; +// let bigStart; +// if (start !== undefined) { +// bigStart = BigInt(start); +// } +// const transactions = await withResponseError( +// client.getAccountTransactions(address, {start: bigStart, limit}), +// ); + +// // Sort in descending order +// transactions.sort(sortTransactions); + +// return transactions; +// } + +// export function getTransaction( +// requestParameters: {txnHashOrVersion: string | number}, +// nodeUrl: string, +// ): Promise { +// const {txnHashOrVersion} = requestParameters; +// if (typeof txnHashOrVersion === "number" || isNumeric(txnHashOrVersion)) { +// const version = +// typeof txnHashOrVersion === "number" +// ? txnHashOrVersion +// : parseInt(txnHashOrVersion); +// return getTransactionByVersion(version, nodeUrl); +// } else { +// return getTransactionByHash(txnHashOrVersion as string, nodeUrl); +// } +// } + +// function getTransactionByVersion( +// version: number, +// nodeUrl: string, +// ): Promise { +// const client = new AptosClient(nodeUrl); +// return withResponseError(client.getTransactionByVersion(BigInt(version))); +// } + +// function getTransactionByHash( +// hash: string, +// nodeUrl: string, +// ): Promise { +// const client = new AptosClient(nodeUrl); +// return withResponseError(client.getTransactionByHash(hash)); +// } + +// export function getLedgerInfo(nodeUrl: string): Promise { +// const client = new AptosClient(nodeUrl); +// return withResponseError(client.getLedgerInfo()); +// } + +// export function getLedgerInfoWithoutResponseError( +// nodeUrl: string, +// ): Promise { +// const client = new AptosClient(nodeUrl); +// return client.getLedgerInfo(); +// } + +// export function getAccount( +// requestParameters: {address: string}, +// nodeUrl: string, +// ): Promise { +// const client = new AptosClient(nodeUrl); +// const {address} = requestParameters; +// return withResponseError(client.getAccount(address)); +// } + +export function getAccountResources( + requestParameters: {address: string; ledgerVersion?: number}, + nodeUrl: string, +): Promise { + const client = new AptosClient(nodeUrl); + const {address, ledgerVersion} = requestParameters; + let ledgerVersionBig; + if (ledgerVersion !== undefined) { + ledgerVersionBig = BigInt(ledgerVersion); + } + return withResponseError( + client.getAccountResources(address, {ledgerVersion: ledgerVersionBig}), + ); +} + +// export function getAccountResource( +// requestParameters: { +// address: string; +// resourceType: string; +// ledgerVersion?: number; +// }, +// nodeUrl: string, +// ): Promise { +// const client = new AptosClient(nodeUrl); +// const {address, resourceType, ledgerVersion} = requestParameters; +// let ledgerVersionBig; +// if (ledgerVersion !== undefined) { +// ledgerVersionBig = BigInt(ledgerVersion); +// } +// return withResponseError( +// client.getAccountResource(address, resourceType, { +// ledgerVersion: ledgerVersionBig, +// }), +// ); +// } + +export function getAccountModules( + requestParameters: {address: string; ledgerVersion?: number}, + nodeUrl: string, +): Promise { + const client = new AptosClient(nodeUrl); + const {address, ledgerVersion} = requestParameters; + let ledgerVersionBig; + if (ledgerVersion !== undefined) { + ledgerVersionBig = BigInt(ledgerVersion); + } + return withResponseError( + client.getAccountModules(address, {ledgerVersion: ledgerVersionBig}), + ); +} + +export function getAccountModule( + requestParameters: { + address: string; + moduleName: string; + ledgerVersion?: number; + }, + nodeUrl: string, +): Promise { + const client = new AptosClient(nodeUrl); + const {address, moduleName, ledgerVersion} = requestParameters; + let ledgerVersionBig; + if (ledgerVersion !== undefined) { + ledgerVersionBig = BigInt(ledgerVersion); + } + return withResponseError( + client.getAccountModule(address, moduleName, { + ledgerVersion: ledgerVersionBig, + }), + ); +} + +export function view( + request: Types.ViewRequest, + nodeUrl: string, + ledgerVersion?: string, +): Promise { + const client = new AptosClient(nodeUrl); + let parsedVersion = ledgerVersion; + + // Handle non-numbers, to default to the latest ledger version + if (typeof ledgerVersion === "string" && isNaN(parseInt(ledgerVersion, 10))) { + parsedVersion = undefined; + } + + return client.view(request, parsedVersion); +} + +// export function getTableItem( +// requestParameters: {tableHandle: string; data: Types.TableItemRequest}, +// nodeUrl: string, +// ): Promise { +// const client = new AptosClient(nodeUrl); +// const {tableHandle, data} = requestParameters; +// return withResponseError(client.getTableItem(tableHandle, data)); +// } + +// export function getBlockByHeight( +// requestParameters: {height: number; withTransactions: boolean}, +// nodeUrl: string, +// ): Promise { +// const {height, withTransactions} = requestParameters; +// const client = new AptosClient(nodeUrl); +// return withResponseError(client.getBlockByHeight(height, withTransactions)); +// } + +// export function getBlockByVersion( +// requestParameters: {version: number; withTransactions: boolean}, +// nodeUrl: string, +// ): Promise { +// const {version, withTransactions} = requestParameters; +// const client = new AptosClient(nodeUrl); +// return withResponseError(client.getBlockByVersion(version, withTransactions)); +// } + +// export async function getRecentBlocks( +// currentBlockHeight: number, +// count: number, +// nodeUrl: string, +// ): Promise { +// const client = new AptosClient(nodeUrl); +// const blocks = []; +// for (let i = 0; i < count; i++) { +// const block = await client.getBlockByHeight(currentBlockHeight - i, false); +// blocks.push(block); +// } +// return blocks; +// } + +// export async function getStake( +// client: AptosClient, +// delegatorAddress: Types.Address, +// validatorAddress: Types.Address, +// ): Promise { +// const payload: Types.ViewRequest = { +// function: "0x1::delegation_pool::get_stake", +// type_arguments: [], +// arguments: [validatorAddress, delegatorAddress], +// }; +// return withResponseError(client.view(payload)); +// } + +// export async function getValidatorCommission( +// client: AptosClient, +// validatorAddress: Types.Address, +// ): Promise { +// const payload: Types.ViewRequest = { +// function: "0x1::delegation_pool::operator_commission_percentage", +// type_arguments: [], +// arguments: [validatorAddress], +// }; +// return withResponseError(client.view(payload)); +// } + +// export async function getValidatorCommissionChange( +// client: AptosClient, +// validatorAddress: Types.Address, +// ): Promise { +// const payload: Types.ViewRequest = { +// function: +// "0x1::delegation_pool::operator_commission_percentage_next_lockup_cycle", +// type_arguments: [], +// arguments: [validatorAddress], +// }; +// return withResponseError(client.view(payload)); +// } + +// export async function getDelegationPoolExist( +// client: AptosClient, +// validatorAddress: Types.Address, +// ): Promise { +// const payload: Types.ViewRequest = { +// function: "0x1::delegation_pool::delegation_pool_exists", +// type_arguments: [], +// arguments: [validatorAddress], +// }; +// return withResponseError(client.view(payload)); +// } + +// // Return whether `pending_inactive` stake can be directly withdrawn from the delegation pool, +// // for the edge case when the validator had gone inactive before its lockup expired. +// export async function getCanWithdrawPendingInactive( +// client: AptosClient, +// validatorAddress: Types.Address, +// ): Promise { +// const payload: Types.ViewRequest = { +// function: "0x1::delegation_pool::can_withdraw_pending_inactive", +// type_arguments: [], +// arguments: [validatorAddress], +// }; +// return withResponseError(client.view(payload)); +// } + +// export async function getAddStakeFee( +// client: AptosClient, +// validatorAddress: Types.Address, +// amount: string, +// ): Promise { +// const payload: Types.ViewRequest = { +// function: "0x1::delegation_pool::get_add_stake_fee", +// type_arguments: [], +// arguments: [validatorAddress, (Number(amount) * OCTA).toString()], +// }; +// return withResponseError(client.view(payload)); +// } + +// export async function getValidatorState( +// client: AptosClient, +// validatorAddress: Types.Address, +// ): Promise { +// const payload: Types.ViewRequest = { +// function: "0x1::stake::get_validator_state", +// type_arguments: [], +// arguments: [validatorAddress], +// }; +// return withResponseError(client.view(payload)); +// } diff --git a/packages/nextjs/hooks/scaffold-eth/useDeployedContractInfo.ts b/packages/nextjs/hooks/scaffold-eth/useDeployedContractInfo.ts index 8f649c3..7b41c69 100644 --- a/packages/nextjs/hooks/scaffold-eth/useDeployedContractInfo.ts +++ b/packages/nextjs/hooks/scaffold-eth/useDeployedContractInfo.ts @@ -2,7 +2,7 @@ import { useEffect, useState } from "react"; import { useTargetNetwork } from "./useTargetNetwork"; import { useIsMounted } from "usehooks-ts"; import { usePublicClient } from "wagmi"; -import { Contract, ContractCodeStatus, ContractName, contracts } from "~~/utils/scaffold-eth/contract"; +import { Contract, ContractCodeStatus, ContractName, contracts } from "~~/utils/scaffold-move/contract"; /** * Gets the matching contract info for the provided contract name from the contracts present in deployedContracts.ts @@ -10,30 +10,31 @@ import { Contract, ContractCodeStatus, ContractName, contracts } from "~~/utils/ */ export const useDeployedContractInfo = (contractName: TContractName) => { const isMounted = useIsMounted(); - const { targetNetwork } = useTargetNetwork(); - const deployedContract = contracts?.[targetNetwork.id]?.[contractName as ContractName] as Contract; + const targetNetwork = "devnet"; + const deployedContract = contracts?.[targetNetwork]?.[contractName.toString()] as Contract; + console.log("deployedContract", deployedContract); const [status, setStatus] = useState(ContractCodeStatus.LOADING); - const publicClient = usePublicClient({ chainId: targetNetwork.id }); + // const publicClient = usePublicClient({ chainId: targetNetwork.id }); useEffect(() => { const checkContractDeployment = async () => { try { - if (!isMounted() || !publicClient) return; + // if (!isMounted() || !publicClient) return; if (!deployedContract) { setStatus(ContractCodeStatus.NOT_FOUND); return; } - const code = await publicClient.getBytecode({ - address: deployedContract.address, - }); + // const code = await publicClient.getBytecode({ + // address: deployedContract.address, + // }); - // If contract code is `0x` => no contract deployed on that address - if (code === "0x") { - setStatus(ContractCodeStatus.NOT_FOUND); - return; - } + // // If contract code is `0x` => no contract deployed on that address + // if (code === "0x") { + // setStatus(ContractCodeStatus.NOT_FOUND); + // return; + // } setStatus(ContractCodeStatus.DEPLOYED); } catch (e) { console.error(e); @@ -42,7 +43,7 @@ export const useDeployedContractInfo = (cont }; checkContractDeployment(); - }, [isMounted, contractName, deployedContract, publicClient]); + }, [isMounted, contractName, deployedContract]); return { data: status === ContractCodeStatus.DEPLOYED ? deployedContract : undefined, diff --git a/packages/nextjs/hooks/scaffold-move/useGetAccountAPTBalance.ts b/packages/nextjs/hooks/scaffold-move/useGetAccountAPTBalance.ts new file mode 100644 index 0000000..c8d65d6 --- /dev/null +++ b/packages/nextjs/hooks/scaffold-move/useGetAccountAPTBalance.ts @@ -0,0 +1,30 @@ +import {Types} from "aptos"; +import {useGetAccountResources} from "./useGetAccountResources"; + +interface CoinStore { + coin: { + value: string; + }; +} + +export function useGetAccountAPTBalance(address: string) { + console.log("useGetAccountAPTBalance", address); + const {isLoading, data, error} = useGetAccountResources(address); + + if (isLoading || error || !data) { + return null; + } + + const coinStore = data.find( + (resource) => + resource.type === "0x1::coin::CoinStore<0x1::aptos_coin::AptosCoin>", + ); + + if (!coinStore) { + return null; + } + + const coinStoreData: CoinStore = coinStore.data as CoinStore; + + return coinStoreData?.coin?.value; +} diff --git a/packages/nextjs/hooks/scaffold-move/useGetAccountModules.ts b/packages/nextjs/hooks/scaffold-move/useGetAccountModules.ts new file mode 100644 index 0000000..395a14d --- /dev/null +++ b/packages/nextjs/hooks/scaffold-move/useGetAccountModules.ts @@ -0,0 +1,16 @@ +import {Types} from "aptos"; +import {useQuery, UseQueryResult} from "@tanstack/react-query"; +import {getAccountModules} from ".."; +import {ResponseError} from "../client"; +import {useGlobalState} from "../../global-config/GlobalConfig"; + +export function useGetAccountModules( + address: string, +): UseQueryResult { + const [state] = useGlobalState(); + + return useQuery, ResponseError>({ + queryKey: ["accountModules", {address}, state.network_value], + queryFn: () => getAccountModules({address}, state.network_value), + }); +} diff --git a/packages/nextjs/hooks/scaffold-move/useGetAccountResources.ts b/packages/nextjs/hooks/scaffold-move/useGetAccountResources.ts new file mode 100644 index 0000000..b6c7b3e --- /dev/null +++ b/packages/nextjs/hooks/scaffold-move/useGetAccountResources.ts @@ -0,0 +1,20 @@ +import {Types} from "aptos"; +import {useQuery, UseQueryResult} from "@tanstack/react-query"; +import {getAccountResources} from ".."; +import {ResponseError} from "../client"; +import {useGlobalState} from "../../global-config/GlobalConfig"; + +export function useGetAccountResources( + address: string, + options?: { + retry?: number | boolean; + }, +): UseQueryResult { + const [state] = useGlobalState(); + const test = useQuery, ResponseError>({ + queryKey: ["accountResources", {address}, state.network_value], + queryFn: () => getAccountResources({address}, state.network_value), + retry: options?.retry ?? false, + }); + return test; +} diff --git a/packages/nextjs/hooks/scaffold-move/useGraphqlClient.tsx b/packages/nextjs/hooks/scaffold-move/useGraphqlClient.tsx new file mode 100644 index 0000000..8b196eb --- /dev/null +++ b/packages/nextjs/hooks/scaffold-move/useGraphqlClient.tsx @@ -0,0 +1,81 @@ +import React from "react"; +import { + ApolloClient, + InMemoryCache, + ApolloProvider, + HttpLink, + NormalizedCacheObject, +} from "@apollo/client"; +import {useEffect, useState} from "react"; +import {NetworkName} from "../../constants"; +import {useGlobalState} from "../../global-config/GlobalConfig"; + +function getIsGraphqlClientSupportedFor(networkName: NetworkName): boolean { + const graphqlUri = getGraphqlURI(networkName); + return typeof graphqlUri === "string" && graphqlUri.length > 0; +} + +export function getGraphqlURI(networkName: NetworkName): string | undefined { + switch (networkName) { + case "mainnet": + return "https://api.mainnet.aptoslabs.com/v1/graphql"; + case "testnet": + return "https://api-staging.testnet.aptoslabs.com/v1/graphql"; + case "devnet": + return "https://api-staging.devnet.aptoslabs.com/v1/graphql"; + case "local": + return "http://127.0.0.1:8090/v1/graphql"; + case "randomnet": + return "https://indexer.random.aptoslabs.com/v1/graphql"; + default: + return undefined; + } +} + +function getGraphqlClient( + networkName: NetworkName, +): ApolloClient { + return new ApolloClient({ + link: new HttpLink({ + uri: getGraphqlURI(networkName), + }), + cache: new InMemoryCache(), + }); +} + +export function useGetGraphqlClient() { + const [state] = useGlobalState(); + const [graphqlClient, setGraphqlClient] = useState< + ApolloClient + >(getGraphqlClient(state.network_name)); + + useEffect(() => { + setGraphqlClient(getGraphqlClient(state.network_name)); + }, [state.network_name]); + + return graphqlClient; +} + +type GraphqlClientProviderProps = { + children: React.ReactNode; +}; + +export function GraphqlClientProvider({children}: GraphqlClientProviderProps) { + const graphqlClient = useGetGraphqlClient(); + + return {children}; +} + +export function useGetIsGraphqlClientSupported(): boolean { + const [state] = useGlobalState(); + const [isGraphqlClientSupported, setIsGraphqlClientSupported] = + useState(getIsGraphqlClientSupportedFor(state.network_name)); + + useEffect(() => { + setIsGraphqlClientSupported( + getIsGraphqlClientSupportedFor(state.network_name), + ); + }, [state.network_name]); + + return isGraphqlClientSupported; +} diff --git a/packages/nextjs/hooks/scaffold-move/useSubmitTransaction.ts b/packages/nextjs/hooks/scaffold-move/useSubmitTransaction.ts new file mode 100644 index 0000000..eab968d --- /dev/null +++ b/packages/nextjs/hooks/scaffold-move/useSubmitTransaction.ts @@ -0,0 +1,116 @@ +import {FailedTransactionError} from "aptos"; +import {useEffect, useState} from "react"; +import { + useWallet, + InputTransactionData, +} from "@aptos-labs/wallet-adapter-react"; +import {useGlobalState} from "../../global-config/GlobalConfig"; + +export type TransactionResponse = + | TransactionResponseOnSubmission + | TransactionResponseOnError; + +// "submission" here means that the transaction is posted on chain and gas is paid. +// However, the status of the transaction might not be "success". +export type TransactionResponseOnSubmission = { + transactionSubmitted: true; + transactionHash: string; + success: boolean; // indicates if the transaction submitted but failed or not + message?: string; // error message if the transaction failed +}; + +export type TransactionResponseOnError = { + transactionSubmitted: false; + message: string; +}; + +const useSubmitTransaction = () => { + const [transactionResponse, setTransactionResponse] = + useState(null); + const [transactionInProcess, setTransactionInProcess] = + useState(false); + const [state] = useGlobalState(); + const {signAndSubmitTransaction, network} = useWallet(); + + useEffect(() => { + if (transactionResponse !== null) { + setTransactionInProcess(false); + } + }, [transactionResponse]); + + async function submitTransaction(transaction: InputTransactionData) { + console.log("network", network?.name, state.network_name); + // if ( + // network?.name.toLocaleLowerCase() !== + // (state.network_name === "local" ? "localhost" : state.network_name) + // ) { + // setTransactionResponse({ + // transactionSubmitted: false, + // message: + // "Wallet and Explorer should use the same network to submit a transaction", + // }); + // return; + // } + + + setTransactionInProcess(true); + console.log("submitting transaction", transaction); + const signAndSubmitTransactionCall = async ( + transaction: InputTransactionData, + ): Promise => { + const responseOnError: TransactionResponseOnError = { + transactionSubmitted: false, + message: "Unknown Error", + }; + + let response; + try { + response = await signAndSubmitTransaction(transaction); + console.log("response", response); + + // transaction submit succeed + if ("hash" in response) { + await state.aptos_client.waitForTransaction(response["hash"], { + checkSuccess: true, + }); + return { + transactionSubmitted: true, + transactionHash: response["hash"], + success: true, + }; + } + // transaction failed + return {...responseOnError, message: response.message}; + } catch (error) { + if (error instanceof FailedTransactionError) { + return { + transactionSubmitted: true, + transactionHash: response ? response.hash : "", + message: error.message, + success: false, + }; + } else if (error instanceof Error) { + return {...responseOnError, message: error.message}; + } + } + return responseOnError; + }; + + await signAndSubmitTransactionCall(transaction).then( + setTransactionResponse, + ); + } + + function clearTransactionResponse() { + setTransactionResponse(null); + } + + return { + submitTransaction, + transactionInProcess, + transactionResponse, + clearTransactionResponse, + }; +}; + +export default useSubmitTransaction; diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json index a6c02b5..ed4894c 100644 --- a/packages/nextjs/package.json +++ b/packages/nextjs/package.json @@ -14,6 +14,7 @@ "vercel:yolo": "vercel --build-env NEXT_PUBLIC_IGNORE_BUILD_ERROR=true" }, "dependencies": { + "@apollo/client": "^3.10.8", "@aptos-labs/wallet-adapter-ant-design": "^2.6.2", "@aptos-labs/wallet-adapter-react": "^3.4.2", "@heroicons/react": "^2.0.11", @@ -25,6 +26,7 @@ "blo": "^1.0.1", "burner-connector": "^0.0.8", "daisyui": "4.5.0", + "graphql": "^16.9.0", "next": "^14.0.4", "next-themes": "^0.2.1", "nprogress": "^0.2.0", @@ -33,7 +35,9 @@ "react": "^18.2.0", "react-copy-to-clipboard": "^5.1.0", "react-dom": "^18.2.0", + "react-hook-form": "^7.52.1", "react-hot-toast": "^2.4.0", + "react-router-dom": "^6.24.1", "use-debounce": "^8.0.4", "usehooks-ts": "^2.13.0", "viem": "2.13.6", diff --git a/packages/nextjs/utils/scaffold-eth/contract.ts b/packages/nextjs/utils/scaffold-eth/contract.ts index 68e494c..b8ea655 100644 --- a/packages/nextjs/utils/scaffold-eth/contract.ts +++ b/packages/nextjs/utils/scaffold-eth/contract.ts @@ -24,8 +24,8 @@ import { import { Config, UseReadContractParameters, UseWatchContractEventParameters } from "wagmi"; import { WriteContractParameters, WriteContractReturnType } from "wagmi/actions"; import { WriteContractVariables } from "wagmi/query"; -import deployedContractsData from "~~/contracts/deployedContracts"; -import externalContractsData from "~~/contracts/externalContracts"; +import deployedContractsData from "~~/contracts/deployedModules"; +import externalContractsData from "~~/contracts/externalModules"; import scaffoldConfig from "~~/scaffold.config"; type AddExternalFlag = { diff --git a/packages/nextjs/utils/scaffold-eth/decodeTxData.ts b/packages/nextjs/utils/scaffold-eth/decodeTxData.ts index 65c20f7..5b9a3b8 100644 --- a/packages/nextjs/utils/scaffold-eth/decodeTxData.ts +++ b/packages/nextjs/utils/scaffold-eth/decodeTxData.ts @@ -2,7 +2,7 @@ import { TransactionWithFunction } from "./block"; import { GenericContractsDeclaration } from "./contract"; import { Abi, AbiFunction, decodeFunctionData, getAbiItem } from "viem"; import { hardhat } from "viem/chains"; -import contractData from "~~/contracts/deployedContracts"; +import contractData from "~~/contracts/deployedModules"; type ContractsInterfaces = Record; type TransactionType = TransactionWithFunction | null; diff --git a/packages/nextjs/utils/scaffold-move/ContentValue/CurrencyValue.tsx b/packages/nextjs/utils/scaffold-move/ContentValue/CurrencyValue.tsx new file mode 100644 index 0000000..8c2836b --- /dev/null +++ b/packages/nextjs/utils/scaffold-move/ContentValue/CurrencyValue.tsx @@ -0,0 +1,97 @@ +import React from "react"; + +const APTOS_DECIMALS = 8; + +function trimRight(rightSide: string) { + while (rightSide.endsWith("0")) { + rightSide = rightSide.slice(0, -1); + } + return rightSide; +} + +export function getFormattedBalanceStr( + balance: string, + decimals?: number, + fixedDecimalPlaces?: number, +): string { + // If balance is zero or decimals is 0, just return it + if (balance == "0" || (decimals !== undefined && decimals === 0)) { + return balance; + } + + const len = balance.length; + decimals = decimals || APTOS_DECIMALS; + + // If length is less than decimals, pad with 0s to decimals length and return + if (len <= decimals) { + return "0." + (trimRight("0".repeat(decimals - len) + balance) || "0"); + } + + // Otherwise, insert decimal point at len - decimals + const leftSide = BigInt(balance.slice(0, len - decimals)).toLocaleString( + "en-US", + ); + let rightSide = balance.slice(len - decimals); + if (BigInt(rightSide) == BigInt(0)) { + return leftSide; + } + + // remove trailing 0s + rightSide = trimRight(rightSide); + if ( + fixedDecimalPlaces !== undefined && + rightSide.length > fixedDecimalPlaces + ) { + rightSide = rightSide.slice(0, fixedDecimalPlaces - rightSide.length); + } + + if (rightSide.length === 0 || rightSide === "0") { + return leftSide; + } + + return leftSide + "." + trimRight(rightSide); +} + +type CurrencyValueProps = { + amount: string; + decimals?: number; + fixedDecimalPlaces?: number; + currencyCode?: string | React.ReactNode; +}; + +export default function CurrencyValue({ + amount, + decimals, + fixedDecimalPlaces, + currencyCode, +}: CurrencyValueProps) { + const number = getFormattedBalanceStr(amount, decimals, fixedDecimalPlaces); + if (currencyCode) { + return ( + + {number} {currencyCode} + + ); + } else { + return {number}; + } +} + +export function APTCurrencyValue({ + amount: amountStr, + decimals, + fixedDecimalPlaces, +}: CurrencyValueProps) { + // remove leading "-" when it's a negative number + let amount = amountStr; + if (amountStr.startsWith("-")) { + amount = amountStr.substring(1); + } + + return ( + + ); +} diff --git a/packages/nextjs/utils/scaffold-move/contract.ts b/packages/nextjs/utils/scaffold-move/contract.ts new file mode 100644 index 0000000..4238042 --- /dev/null +++ b/packages/nextjs/utils/scaffold-move/contract.ts @@ -0,0 +1,103 @@ +import type { MergeDeepRecord } from "type-fest/source/merge-deep"; +import deployedContractsData from "~~/contracts/deployedModules"; +import externalContractsData from "~~/contracts/externalModules"; +import scaffoldConfig from "~~/scaffold.config"; + + +type AddExternalFlag = { + [ChainId in keyof T]: { + [ContractName in keyof T[ChainId]]: T[ChainId][ContractName] & { external?: true }; + }; +}; + +const deepMergeContracts = , E extends Record>( + local: L, + external: E, +) => { + const result: Record = {}; + const allKeys = Array.from(new Set([...Object.keys(external), ...Object.keys(local)])); + for (const key of allKeys) { + if (!external[key]) { + result[key] = local[key]; + continue; + } + const amendedExternal = Object.fromEntries( + Object.entries(external[key] as Record>).map(([contractName, declaration]) => [ + contractName, + { ...declaration, external: true }, + ]), + ); + result[key] = { ...local[key], ...amendedExternal }; + } + return result as MergeDeepRecord, AddExternalFlag, { arrayMergeMode: "replace" }>; +}; + +const contractsData = deepMergeContracts(deployedContractsData, externalContractsData); + +type MoveFunction = { + name: string; + visibility: string; + is_entry: boolean; + is_view: boolean; + generic_type_params: any[]; + params: string[]; + return: string[]; +}; + +type MoveStructField = { + name: string; + type: string; +}; + +type MoveStruct = { + name: string; + is_native: boolean; + abilities: string[]; + generic_type_params: any[]; + fields: MoveStructField[]; +}; + +export type GenericContract = { + bytecode: string; + abi?: GenericContractAbi; + external?: true; +}; + +export type GenericContractAbi = { + address: string; // TODO: address type + name: string; + friends: string[]; //TODO: check which type? + exposed_functions: MoveFunction[]; + structs: MoveStruct[]; +} + +export type GenericContractsDeclaration = { + [chainId: string]: { + [contractName: string]: GenericContract; + }; +}; + +export const contracts = contractsData as GenericContractsDeclaration | null; + + +type ConfiguredChainId = (typeof scaffoldConfig)["targetNetworks"][0]["id"]; + +type IsContractDeclarationMissing = typeof contractsData extends { [key in ConfiguredChainId]: any } + ? TNo + : TYes; + +type ContractsDeclaration = IsContractDeclarationMissing; + +type Contracts = ContractsDeclaration[ConfiguredChainId]; + + +export type ContractName = keyof Contracts; +export type Contract = Contracts[TContractName]; + + + +export enum ContractCodeStatus { + "LOADING", + "DEPLOYED", + "NOT_FOUND", +} diff --git a/packages/nextjs/utils/scaffold-move/contractsData.ts b/packages/nextjs/utils/scaffold-move/contractsData.ts new file mode 100644 index 0000000..5a638c4 --- /dev/null +++ b/packages/nextjs/utils/scaffold-move/contractsData.ts @@ -0,0 +1,7 @@ +import scaffoldConfig from "~~/scaffold.config"; +import { contracts } from "~~/utils/scaffold-move/contract"; + +export function getAllContracts() { + const contractsData = contracts?.["devnet"]; + return contractsData ? contractsData : {}; +} diff --git a/packages/nextjs/utils/utils.ts b/packages/nextjs/utils/utils.ts new file mode 100644 index 0000000..db9513b --- /dev/null +++ b/packages/nextjs/utils/utils.ts @@ -0,0 +1,282 @@ +import {HexString, Types} from "aptos"; +// import pako from "pako"; +// import {Statsig} from "statsig-react"; +/** + * Helper function for exhaustiveness checks. + * + * Hint: If this function is causing a type error, check to make sure that your + * switch statement covers all cases! + */ +// export function assertNever(x: never): never { +// throw new Error("Unexpected object: " + x); +// } + +// /* +// If the transaction doesn't have a version property, +// that means it's a pending transaction (and thus it's expected version will be higher than any existing versions). +// We can consider the version to be Infinity for this case. +// */ +// export function sortTransactions( +// a: Types.Transaction, +// b: Types.Transaction, +// ): number { +// const first = "version" in a ? parseInt(a.version) : Infinity; +// const second = "version" in b ? parseInt(b.version) : Infinity; +// return first < second ? 1 : -1; +// } + +// /* +// Converts a utf8 string encoded as hex back to string +// if hex starts with 0x - ignore this part and start from the 3rd char (at index 2). +// */ +// export function hex_to_string(hex: string): string { +// const hexString = hex.toString(); +// let str = ""; +// let n = hex.startsWith("0x") ? 2 : 0; +// for (n; n < hexString.length; n += 2) { +// str += String.fromCharCode(parseInt(hexString.substring(n, n + 2), 16)); +// } +// return str; +// } + +// /* set localStorage with Expiry */ +// export function setLocalStorageWithExpiry( +// key: string, +// value: string, +// ttl: number, +// ) { +// const now = new Date(); + +// const item = { +// value: value, +// expiry: now.getTime() + ttl, +// }; + +// localStorage.setItem(key, JSON.stringify(item)); +// } + +// /* get localStorage with Expiry */ +// export function getLocalStorageWithExpiry(key: string) { +// const itemStr = localStorage.getItem(key); + +// if (!itemStr) { +// return null; +// } + +// const item = JSON.parse(itemStr); +// const now = new Date(); + +// if (now.getTime() > item.expiry) { +// localStorage.removeItem(key); +// return null; +// } + +// return item.value; +// } + +// export async function fetchJsonResponse(url: string) { +// const response = await fetch(url); +// return await response.json(); +// } + +// /** +// * Convert a module source code in gzipped hex string to plain text +// * @param source module source code in gzipped hex string +// * @returns original source code in plain text +// */ +// export function transformCode(source: string): string { +// try { +// return pako.ungzip(new HexString(source).toUint8Array(), {to: "string"}); +// } catch { +// return ""; +// } +// } + +// export function getBytecodeSizeInKB(bytecodeHex: string): number { +// // Convert the hex string to a byte array +// const textEncoder = new TextEncoder(); +// const byteArray = new Uint8Array(textEncoder.encode(bytecodeHex)); + +// // Compute the size of the byte array in kilobytes (KB) +// const sizeInKB = byteArray.length / 1024; + +// // Return the size in KB with two decimal places +// return parseFloat(sizeInKB.toFixed(2)); +// } + +// /** +// * Standardizes an address to the format "0x" followed by 64 lowercase hexadecimal digits. +// */ +// export const standardizeAddress = (address: string): string => { +// // Convert the address to lowercase +// address = address.toLowerCase(); +// // If the address has more than 66 characters, it's already invalid +// if (address.length > 66) { +// return address; +// } +// // Remove the "0x" prefix if present +// const addressWithoutPrefix = address.startsWith("0x") +// ? address.slice(2) +// : address; +// // If the address has more than 64 characters after removing the prefix, it's already invalid +// if (addressWithoutPrefix.length > 64) { +// return address; +// } +// // Pad the address with leading zeros if necessary to ensure it has exactly 64 characters (excluding the "0x" prefix) +// const addressWithPadding = addressWithoutPrefix.padStart(64, "0"); +// // Return the standardized address with the "0x" prefix +// return "0x" + addressWithPadding; +// }; + +// // inspired by https://stackoverflow.com/questions/3446170/escape-string-for-use-in-javascript-regex +// function escapeRegExp(regexpString: string) { +// return regexpString.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +// } + +// // Get the line number of a public function in a source code. +// // The line number is zero-based. +// // Return 0 if the function is not found. +// export function getPublicFunctionLineNumber( +// sourceCode: string, +// functionName: string, +// ) { +// const lines = sourceCode.split("\n"); +// const publicEntryFunRegexp = new RegExp( +// `\\s*public\\s*(entry\\s*)?fun\\s*${escapeRegExp( +// functionName, +// )}\\s*(?:<|\\()`, +// ); + +// const lineNumber = lines.findIndex((line) => +// line.match(publicEntryFunRegexp), +// ); +// if (lineNumber !== -1) { +// return lineNumber; +// } + +// return 0; +// } + +export function encodeInputArgsForViewRequest(type: string, value: string) { + if (type.includes("vector")) { + // when it's a vector, we support both hex and javascript array format + return value.trim().startsWith("0x") + ? value.trim() + : encodeVectorForViewRequest(type, value); + } else if (type === "bool") { + if (value !== "true" && value !== "false") + throw new Error(`Invalid bool value: ${value}`); + + return value === "true" ? true : false; + } else if (["u8", "u16", "u32"].includes(type)) { + return ensureNumber(value); + } else if (type.startsWith("0x1::option::Option")) { + return {vec: [...(value ? [value] : [])]}; + } else return value; +} + +// Deserialize "[1,2,3]" or "1,2,3" to ["1", "2", "3"] +export function deserializeVector(vectorString: string): string[] { + let result = vectorString.trim(); + if (result[0] === "[" && result[result.length - 1] === "]") { + result = result.slice(1, -1); + } + // There's a tradeoff here between empty string, and empty array. We're going with empty array. + if (result.length == 0) { + return []; + } + return result.split(","); +} + +function encodeVectorForViewRequest(type: string, value: string) { + const rawVector = deserializeVector(value); + const regex = /vector<([^]+)>/; + const match = type.match(regex); + if (match) { + if (match[1] === "u8") { + return ( + HexString.fromUint8Array( + new Uint8Array( + rawVector.map((v) => { + const result = ensureNumber(v.trim()); + if (result < 0 || result > 255) + throw new Error(`Invalid u8 value: ${result}`); + return result; + }), + ), + ) as any + ).hexString; + } else if (["u16", "u32"].includes(match[1])) { + return rawVector.map((v) => ensureNumber(v.trim())); + } else if (["u64", "u128", "u256"].includes(match[1])) { + // For bigint, not need to convert, only validation + rawVector.forEach((v) => ensureBigInt(v.trim())); + return rawVector; + } else if (match[1] === "bool") { + return rawVector.map((v) => ensureBoolean(v.trim())); + } else { + // 1. Address type no need to convert + // 2. Other complex types like Struct is not support yet. We just pass what user input. + return rawVector; + } + } else { + throw new Error(`Unsupported type: ${type}`); + } +} + +function ensureNumber(val: number | string): number { + assertType(val, ["number", "string"]); + if (typeof val === "number") { + return val; + } + + const res = Number.parseInt(val, 10); + if (Number.isNaN(res)) { + throw new Error("Invalid number string."); + } + + return res; +} + +export function ensureBigInt(val: number | bigint | string): bigint { + assertType(val, ["number", "bigint", "string"]); + return BigInt(val); +} + +export function ensureBoolean(val: boolean | string): boolean { + assertType(val, ["boolean", "string"]); + if (typeof val === "boolean") { + return val; + } + + if (val === "true") { + return true; + } + if (val === "false") { + return false; + } + + throw new Error("Invalid boolean string."); +} + +function assertType(val: any, types: string[] | string, message?: string) { + if (!types?.includes(typeof val)) { + throw new Error( + message || + `Invalid arg: ${val} type should be ${ + types instanceof Array ? types.join(" or ") : types + }`, + ); + } +} + +// // We should not be using statsig for logging like this, we will transition to google analytics +// export function getStableID(): string { +// return Statsig.initializeCalled() ? Statsig.getStableID() : "not_initialized"; +// } + +// address' coming back from the node trim leading zeroes +// for example: 0x123 => 0x000...000123 (61 0s before 123) +export function normalizeAddress(address: string): string { + return "0x" + address.substring(2).padStart(64, "0"); +} diff --git a/scripts/loadContracts.js b/scripts/loadContracts.js new file mode 100644 index 0000000..1b03801 --- /dev/null +++ b/scripts/loadContracts.js @@ -0,0 +1,104 @@ +const fs = require('fs'); +const path = require('path'); +const yaml = require('js-yaml'); +const { AptosClient } = require('aptos'); // Assuming you're using the Aptos SDK for JavaScript + +// Paths to the relevant files +const moveTomlPath = path.join(__dirname, '../packages/move/Move.toml'); +const configYamlPath = path.join(__dirname, '../packages/move/.aptos/config.yaml'); +const deployedModulesPath = path.join(__dirname, '../packages/nextjs/contracts/deployedModules.ts'); +const externalModulesPath = path.join(__dirname, '../packages/nextjs/contracts/externalModules.ts'); + +// Function to parse the TOML file and extract addresses +function parseToml(filePath) { + const toml = fs.readFileSync(filePath, 'utf-8'); + const addressesSection = toml.match(/\[addresses\]([\s\S]*?)(?=\[|$)/); + if (addressesSection) { + const addresses = {}; + const lines = addressesSection[1].trim().split('\n'); + lines.forEach(line => { + const [key, value] = line.split('=').map(part => part.trim().replace(/['"]+/g, '')); + addresses[key] = value.replace(/^0x/, ''); // Strip 0x from the address + }); + return addresses; + } + return null; +} + +// Function to parse the YAML config file +function parseYaml(filePath) { + const yamlContent = fs.readFileSync(filePath, 'utf-8'); + return yaml.load(yamlContent); +} + +// Function to write modules to a TypeScript file +function writeModules(filePath, modules, network, variableName) { + const moduleEntries = modules.map(module => { + return `"${module.abi.name}": { + "bytecode": ${JSON.stringify(module.bytecode)}, + "abi": ${JSON.stringify(module.abi)} + }`; + }).join(',\n'); + + const output = ` +import { GenericContractsDeclaration } from "~~/utils/scaffold-move/contract"; + +const ${variableName} = { + "${network}": { + ${moduleEntries} + } +} as const; + +export default ${variableName} satisfies GenericContractsDeclaration; + `; + + fs.writeFileSync(filePath, output.trim(), 'utf-8'); +} + +// Function to fetch account modules +async function getAccountModules(requestParameters, nodeUrl) { + const client = new AptosClient(nodeUrl); + const { address, ledgerVersion } = requestParameters; + let ledgerVersionBig; + if (ledgerVersion !== undefined) { + ledgerVersionBig = BigInt(ledgerVersion); + } + return client.getAccountModules(address, { ledgerVersion: ledgerVersionBig }); +} + +// Main function to perform the tasks +async function main() { + const config = parseYaml(configYamlPath); + const nodeUrl = config.profiles.default.rest_url; + const accountAddress = config.profiles.default.account.replace(/^0x/, ''); // Strip 0x from the account address + + const addresses = parseToml(moveTomlPath); + + // Ensure the output directory exists + const outputDirectory = path.dirname(deployedModulesPath); + if (!fs.existsSync(outputDirectory)) { + fs.mkdirSync(outputDirectory, { recursive: true }); + } + + // Fetch and save account modules for the account from config.yaml + const deployedModules = await getAccountModules({ address: accountAddress }, nodeUrl); + writeModules(deployedModulesPath, deployedModules, "devnet", "deployedContracts"); + console.log(`Data for deployed modules at address ${accountAddress} saved successfully.`); + + // Fetch and save account modules for each address from Move.toml, excluding the one from config.yaml + if (addresses) { + const externalModules = []; + for (const [name, address] of Object.entries(addresses)) { + if (address.toLowerCase() !== accountAddress.toLowerCase()) { + const modules = await getAccountModules({ address }, nodeUrl); + externalModules.push(...modules); + console.log(`Data for address ${address} saved successfully.`); + } + } + writeModules(externalModulesPath, externalModules, "devnet", "externalContracts"); + } else { + console.log('No addresses found in Move.toml.'); + } +} + +main().catch(console.error); diff --git a/yarn.lock b/yarn.lock index 87269ae..7f62f06 100644 --- a/yarn.lock +++ b/yarn.lock @@ -98,6 +98,43 @@ __metadata: languageName: node linkType: hard +"@apollo/client@npm:^3.10.8": + version: 3.10.8 + resolution: "@apollo/client@npm:3.10.8" + dependencies: + "@graphql-typed-document-node/core": ^3.1.1 + "@wry/caches": ^1.0.0 + "@wry/equality": ^0.5.6 + "@wry/trie": ^0.5.0 + graphql-tag: ^2.12.6 + hoist-non-react-statics: ^3.3.2 + optimism: ^0.18.0 + prop-types: ^15.7.2 + rehackt: ^0.1.0 + response-iterator: ^0.2.6 + symbol-observable: ^4.0.0 + ts-invariant: ^0.10.3 + tslib: ^2.3.0 + zen-observable-ts: ^1.2.5 + peerDependencies: + graphql: ^15.0.0 || ^16.0.0 + graphql-ws: ^5.5.5 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + subscriptions-transport-ws: ^0.9.0 || ^0.11.0 + peerDependenciesMeta: + graphql-ws: + optional: true + react: + optional: true + react-dom: + optional: true + subscriptions-transport-ws: + optional: true + checksum: 965e95389bdbde8aa5f542f11860fd930438f635560c766c716f186d5a9d6c5f0bfb31088cf28150c4f3969191030f1bbc0254418738a5737568108e96ffd815 + languageName: node + linkType: hard + "@aptos-connect/wallet-adapter-plugin@npm:^1.0.0": version: 1.0.0 resolution: "@aptos-connect/wallet-adapter-plugin@npm:1.0.0" @@ -823,6 +860,15 @@ __metadata: languageName: node linkType: hard +"@graphql-typed-document-node/core@npm:^3.1.1": + version: 3.2.0 + resolution: "@graphql-typed-document-node/core@npm:3.2.0" + peerDependencies: + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + checksum: fa44443accd28c8cf4cb96aaaf39d144a22e8b091b13366843f4e97d19c7bfeaf609ce3c7603a4aeffe385081eaf8ea245d078633a7324c11c5ec4b2011bb76d + languageName: node + linkType: hard + "@heroicons/react@npm:^2.0.11": version: 2.0.18 resolution: "@heroicons/react@npm:2.0.18" @@ -1887,6 +1933,13 @@ __metadata: languageName: node linkType: hard +"@remix-run/router@npm:1.17.1": + version: 1.17.1 + resolution: "@remix-run/router@npm:1.17.1" + checksum: f6ab2498d0b29ea76e3ddf6c6fec78c99f7c8739b2d413217fa569fca3de2256ecfd71385c78ea9d323fd9787614b25fc13db3aaa2e5d5673c9a698d448e710a + languageName: node + linkType: hard + "@rollup/pluginutils@npm:^4.0.0": version: 4.2.1 resolution: "@rollup/pluginutils@npm:4.2.1" @@ -2009,6 +2062,7 @@ __metadata: version: 0.0.0-use.local resolution: "@se-2/nextjs@workspace:packages/nextjs" dependencies: + "@apollo/client": ^3.10.8 "@aptos-labs/wallet-adapter-ant-design": ^2.6.2 "@aptos-labs/wallet-adapter-react": ^3.4.2 "@heroicons/react": ^2.0.11 @@ -2032,6 +2086,7 @@ __metadata: eslint-config-next: ^14.0.4 eslint-config-prettier: ^9.1.0 eslint-plugin-prettier: ^5.1.3 + graphql: ^16.9.0 next: ^14.0.4 next-themes: ^0.2.1 nprogress: ^0.2.0 @@ -2042,7 +2097,9 @@ __metadata: react: ^18.2.0 react-copy-to-clipboard: ^5.1.0 react-dom: ^18.2.0 + react-hook-form: ^7.52.1 react-hot-toast: ^2.4.0 + react-router-dom: ^6.24.1 tailwindcss: ^3.4.3 type-fest: ^4.6.0 typescript: 5.1.6 @@ -2417,6 +2474,13 @@ __metadata: languageName: node linkType: hard +"@types/js-yaml@npm:^4": + version: 4.0.9 + resolution: "@types/js-yaml@npm:4.0.9" + checksum: e5e5e49b5789a29fdb1f7d204f82de11cb9e8f6cb24ab064c616da5d6e1b3ccfbf95aa5d1498a9fbd3b9e745564e69b4a20b6c530b5a8bbb2d4eb830cda9bc69 + languageName: node + linkType: hard + "@types/json-schema@npm:^7.0.6, @types/json-schema@npm:^7.0.9": version: 7.0.13 resolution: "@types/json-schema@npm:7.0.13" @@ -3433,6 +3497,51 @@ __metadata: languageName: node linkType: hard +"@wry/caches@npm:^1.0.0": + version: 1.0.1 + resolution: "@wry/caches@npm:1.0.1" + dependencies: + tslib: ^2.3.0 + checksum: 9e89aa8e9e08577b2e4acbe805f406b141ae49c2ac4a2e22acf21fbee68339fa0550e0dee28cf2158799f35bb812326e80212e49e2afd169f39f02ad56ae4ef4 + languageName: node + linkType: hard + +"@wry/context@npm:^0.7.0": + version: 0.7.4 + resolution: "@wry/context@npm:0.7.4" + dependencies: + tslib: ^2.3.0 + checksum: 9bc8c30a31f9c7d36b616e89daa9280c03d196576a4f9fef800e9bd5de9434ba70216322faeeacc7ef1ab95f59185599d702538114045df729a5ceea50aef4e2 + languageName: node + linkType: hard + +"@wry/equality@npm:^0.5.6": + version: 0.5.7 + resolution: "@wry/equality@npm:0.5.7" + dependencies: + tslib: ^2.3.0 + checksum: 892f262fae362df80f199b12658ea6966949539d4a3a50c1acf00d94a367d673a38f8efa1abcb726ae9e5cc5e62fce50c540c70f797b7c8a2c4308b401dfd903 + languageName: node + linkType: hard + +"@wry/trie@npm:^0.4.3": + version: 0.4.3 + resolution: "@wry/trie@npm:0.4.3" + dependencies: + tslib: ^2.3.0 + checksum: 106e021125cfafd22250a6631a0438a6a3debae7bd73f6db87fe42aa0757fe67693db0dfbe200ae1f60ba608c3e09ddb8a4e2b3527d56ed0a7e02aa0ee4c94e1 + languageName: node + linkType: hard + +"@wry/trie@npm:^0.5.0": + version: 0.5.0 + resolution: "@wry/trie@npm:0.5.0" + dependencies: + tslib: ^2.3.0 + checksum: 92aeea34152bd8485184236fe328d3d05fc98ee3b431d82ee60cf3584dbf68155419c3d65d0ff3731b204ee79c149440a9b7672784a545afddc8d4342fbf21c9 + languageName: node + linkType: hard + "abbrev@npm:1, abbrev@npm:^1.0.0": version: 1.1.1 resolution: "abbrev@npm:1.1.1" @@ -6639,6 +6748,24 @@ __metadata: languageName: node linkType: hard +"graphql-tag@npm:^2.12.6": + version: 2.12.6 + resolution: "graphql-tag@npm:2.12.6" + dependencies: + tslib: ^2.1.0 + peerDependencies: + graphql: ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + checksum: b15162a3d62f17b9b79302445b9ee330e041582f1c7faca74b9dec5daa74272c906ec1c34e1c50592bb6215e5c3eba80a309103f6ba9e4c1cddc350c46f010df + languageName: node + linkType: hard + +"graphql@npm:^16.9.0": + version: 16.9.0 + resolution: "graphql@npm:16.9.0" + checksum: 8cb3d54100e9227310383ce7f791ca48d12f15ed9f2021f23f8735f1121aafe4e5e611a853081dd935ce221724ea1ae4638faef5d2921fb1ad7c26b5f46611e9 + languageName: node + linkType: hard + "h3@npm:^1.8.1, h3@npm:^1.8.2": version: 1.10.0 resolution: "h3@npm:1.10.0" @@ -6752,6 +6879,15 @@ __metadata: languageName: node linkType: hard +"hoist-non-react-statics@npm:^3.3.2": + version: 3.3.2 + resolution: "hoist-non-react-statics@npm:3.3.2" + dependencies: + react-is: ^16.7.0 + checksum: b1538270429b13901ee586aa44f4cc3ecd8831c061d06cb8322e50ea17b3f5ce4d0e2e66394761e6c8e152cd8c34fb3b4b690116c6ce2bd45b18c746516cb9e8 + languageName: node + linkType: hard + "http-cache-semantics@npm:^4.0.0, http-cache-semantics@npm:^4.1.1": version: 4.1.1 resolution: "http-cache-semantics@npm:4.1.1" @@ -8777,6 +8913,18 @@ __metadata: languageName: node linkType: hard +"optimism@npm:^0.18.0": + version: 0.18.0 + resolution: "optimism@npm:0.18.0" + dependencies: + "@wry/caches": ^1.0.0 + "@wry/context": ^0.7.0 + "@wry/trie": ^0.4.3 + tslib: ^2.3.0 + checksum: d6ed6a90b05ee886dadfe556c7a30227c66843f51278e51eb843977a6a9368b6c50297fcc63fa514f53d8a5a58f8ddc8049c2356bd4ffac32f8961bcb806254d + languageName: node + linkType: hard + "optionator@npm:^0.9.3": version: 0.9.3 resolution: "optionator@npm:0.9.3" @@ -9304,7 +9452,7 @@ __metadata: languageName: node linkType: hard -"prop-types@npm:^15.8.1": +"prop-types@npm:^15.7.2, prop-types@npm:^15.8.1": version: 15.8.1 resolution: "prop-types@npm:15.8.1" dependencies: @@ -10004,6 +10152,15 @@ __metadata: languageName: node linkType: hard +"react-hook-form@npm:^7.52.1": + version: 7.52.1 + resolution: "react-hook-form@npm:7.52.1" + peerDependencies: + react: ^16.8.0 || ^17 || ^18 || ^19 + checksum: 224fec214c5c7093b6949bc0a4fce3cf9b7a567a2f36dc3c7feeb1e721c5cccbd21f0f0ab19aa1f5f912014264f9c2224181370007609693b6c5ef6778f59ca5 + languageName: node + linkType: hard + "react-hot-toast@npm:^2.4.0": version: 2.4.1 resolution: "react-hot-toast@npm:2.4.1" @@ -10016,7 +10173,7 @@ __metadata: languageName: node linkType: hard -"react-is@npm:^16.13.1": +"react-is@npm:^16.13.1, react-is@npm:^16.7.0": version: 16.13.1 resolution: "react-is@npm:16.13.1" checksum: f7a19ac3496de32ca9ae12aa030f00f14a3d45374f1ceca0af707c831b2a6098ef0d6bdae51bd437b0a306d7f01d4677fcc8de7c0d331eb47ad0f46130e53c5f @@ -10078,6 +10235,30 @@ __metadata: languageName: node linkType: hard +"react-router-dom@npm:^6.24.1": + version: 6.24.1 + resolution: "react-router-dom@npm:6.24.1" + dependencies: + "@remix-run/router": 1.17.1 + react-router: 6.24.1 + peerDependencies: + react: ">=16.8" + react-dom: ">=16.8" + checksum: 95d9183524075aeec222b8e3181c47a6f58118a82d8d83dd85bf9f94a6cd69856c71f8f5d9788e50f442b9ea694209db7a96727a394de08c828bbc212328dc95 + languageName: node + linkType: hard + +"react-router@npm:6.24.1": + version: 6.24.1 + resolution: "react-router@npm:6.24.1" + dependencies: + "@remix-run/router": 1.17.1 + peerDependencies: + react: ">=16.8" + checksum: 18acd84a4fc19ef63316a0ed73a549d1d83b48458ca4c90e14b9f59d259984062928f4427307b68f504452eb30dfd373edda826dd21b7f75a4bc2801b7336489 + languageName: node + linkType: hard + "react-style-singleton@npm:^2.2.1": version: 2.2.1 resolution: "react-style-singleton@npm:2.2.1" @@ -10225,6 +10406,21 @@ __metadata: languageName: node linkType: hard +"rehackt@npm:^0.1.0": + version: 0.1.0 + resolution: "rehackt@npm:0.1.0" + peerDependencies: + "@types/react": "*" + react: "*" + peerDependenciesMeta: + "@types/react": + optional: true + react: + optional: true + checksum: 2c3bcd72524bf47672640265e79cba785e0e6837b9b385ccb0a3ea7d00f55a439d9aed3e0ae71e991d88e0d4b2b3158457c92e75fff5ebf99cd46e280068ddeb + languageName: node + linkType: hard + "require-directory@npm:^2.1.1": version: 2.1.1 resolution: "require-directory@npm:2.1.1" @@ -10333,6 +10529,13 @@ __metadata: languageName: node linkType: hard +"response-iterator@npm:^0.2.6": + version: 0.2.6 + resolution: "response-iterator@npm:0.2.6" + checksum: b0db3c0665a0d698d65512951de9623c086b9c84ce015a76076d4bd0bf733779601d0b41f0931d16ae38132fba29e1ce291c1f8e6550fc32daaa2dc3ab4f338d + languageName: node + linkType: hard + "responselike@npm:^2.0.0": version: 2.0.1 resolution: "responselike@npm:2.0.1" @@ -10485,9 +10688,10 @@ __metadata: version: 0.0.0-use.local resolution: "se-2@workspace:." dependencies: - "@aptos-labs/wallet-adapter-ant-design": ^2.6.2 + "@types/js-yaml": ^4 aptos: ^1.21.0 husky: ^8.0.1 + js-yaml: ^4.1.0 lint-staged: ^13.0.3 languageName: unknown linkType: soft @@ -11032,6 +11236,13 @@ __metadata: languageName: node linkType: hard +"symbol-observable@npm:^4.0.0": + version: 4.0.0 + resolution: "symbol-observable@npm:4.0.0" + checksum: 212c7edce6186634d671336a88c0e0bbd626c2ab51ed57498dc90698cce541839a261b969c2a1e8dd43762133d47672e8b62e0b1ce9cf4157934ba45fd172ba8 + languageName: node + linkType: hard + "synckit@npm:^0.8.6": version: 0.8.8 resolution: "synckit@npm:0.8.8" @@ -11244,6 +11455,15 @@ __metadata: languageName: node linkType: hard +"ts-invariant@npm:^0.10.3": + version: 0.10.3 + resolution: "ts-invariant@npm:0.10.3" + dependencies: + tslib: ^2.1.0 + checksum: bb07d56fe4aae69d8860e0301dfdee2d375281159054bc24bf1e49e513fb0835bf7f70a11351344d213a79199c5e695f37ebbf5a447188a377ce0cd81d91ddb5 + languageName: node + linkType: hard + "ts-morph@npm:12.0.0": version: 12.0.0 resolution: "ts-morph@npm:12.0.0" @@ -11325,7 +11545,7 @@ __metadata: languageName: node linkType: hard -"tslib@npm:^2.6.2": +"tslib@npm:^2.3.0, tslib@npm:^2.6.2": version: 2.6.3 resolution: "tslib@npm:2.6.3" checksum: 74fce0e100f1ebd95b8995fbbd0e6c91bdd8f4c35c00d4da62e285a3363aaa534de40a80db30ecfd388ed7c313c42d930ee0eaf108e8114214b180eec3dbe6f5 @@ -12357,6 +12577,22 @@ __metadata: languageName: node linkType: hard +"zen-observable-ts@npm:^1.2.5": + version: 1.2.5 + resolution: "zen-observable-ts@npm:1.2.5" + dependencies: + zen-observable: 0.8.15 + checksum: 3b707b7a0239a9bc40f73ba71b27733a689a957c1f364fabb9fa9cbd7d04b7c2faf0d517bf17004e3ed3f4330ac613e84c0d32313e450ddaa046f3350af44541 + languageName: node + linkType: hard + +"zen-observable@npm:0.8.15": + version: 0.8.15 + resolution: "zen-observable@npm:0.8.15" + checksum: b7289084bc1fc74a559b7259faa23d3214b14b538a8843d2b001a35e27147833f4107590b1b44bf5bc7f6dfe6f488660d3a3725f268e09b3925b3476153b7821 + languageName: node + linkType: hard + "zustand@npm:4.4.1, zustand@npm:^4.1.2": version: 4.4.1 resolution: "zustand@npm:4.4.1"