diff --git a/.gitignore b/.gitignore index 0165c169..92d2e414 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ # Generated files .docusaurus .cache-loader +.yarn # Misc .DS_Store diff --git a/.yarnrc.yml b/.yarnrc.yml new file mode 100644 index 00000000..8b757b29 --- /dev/null +++ b/.yarnrc.yml @@ -0,0 +1 @@ +nodeLinker: node-modules \ No newline at end of file diff --git a/docusaurus.config.js b/docusaurus.config.js index a3cc071a..8c7af8db 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -90,11 +90,7 @@ const config = { docId: "wallet/getting-started/web/getting-started", label: "Wallets", }, - { - href: "https://stackblitz.com/edit/vitejs-vite-71wsul?file=src%2Fmain.ts&terminal=dev", - label: "Playground", - position: "right", - }, + { to: "playground/", label: "Playground", position: "right" }, { href: "https://debug.walletbeacon.io", label: "Debug Wallet", diff --git a/package-lock.json b/package-lock.json index da7fcfa2..695a5a76 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,14 +14,14 @@ "@docusaurus/preset-classic": "2.4.3", "@docusaurus/theme-live-codeblock": "^2.4.3", "@mdx-js/react": "^1.6.22", + "@monaco-editor/react": "^4.6.0", "@taquito/beacon-wallet": "^17.3.1", "@taquito/taquito": "^17.3.1", "clsx": "^1.2.1", "mermaid": "^10.4.0", "prism-react-renderer": "^1.3.5", "react": "^17.0.2", - "react-dom": "^17.0.2", - "react-monaco-editor": "^0.54.0" + "react-dom": "^17.0.2" }, "devDependencies": { "@docusaurus/module-type-aliases": "2.4.3", @@ -3164,6 +3164,30 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/@monaco-editor/loader": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.4.0.tgz", + "integrity": "sha512-00ioBig0x642hytVspPl7DbQyaSWRaolYie/UFNjoTdvoKPzo6xrXLhTk9ixgIKcLH5b5vDOjVNiGyY+uDCUlg==", + "dependencies": { + "state-local": "^1.0.6" + }, + "peerDependencies": { + "monaco-editor": ">= 0.21.0 < 1" + } + }, + "node_modules/@monaco-editor/react": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.6.0.tgz", + "integrity": "sha512-RFkU9/i7cN2bsq/iTkurMWOEErmYcY6JiQI3Jn+WeR/FGISH8JbHERjpS9oRuSOPvDMJI0Z8nJeKkbOs9sBYQw==", + "dependencies": { + "@monaco-editor/loader": "^1.4.0" + }, + "peerDependencies": { + "monaco-editor": ">= 0.25.0 < 1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -13147,19 +13171,6 @@ "webpack": ">=4.41.1 || 5.x" } }, - "node_modules/react-monaco-editor": { - "version": "0.54.0", - "resolved": "https://registry.npmjs.org/react-monaco-editor/-/react-monaco-editor-0.54.0.tgz", - "integrity": "sha512-9JwO69851mfpuhYLHlKbae7omQWJ/2ICE2lbL0VHyNyZR8rCOH7440u+zAtDgiOMpLwmYdY1sEZCdRefywX6GQ==", - "dependencies": { - "prop-types": "^15.8.1" - }, - "peerDependencies": { - "@types/react": ">=16 <= 18", - "monaco-editor": "^0.39.0", - "react": ">=16 <= 18" - } - }, "node_modules/react-router": { "version": "5.3.4", "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.4.tgz", @@ -14475,6 +14486,11 @@ "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==", "deprecated": "Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility" }, + "node_modules/state-local": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz", + "integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==" + }, "node_modules/state-toggle": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/state-toggle/-/state-toggle-1.0.3.tgz", @@ -18765,6 +18781,22 @@ "resolved": "https://registry.npmjs.org/@mdx-js/util/-/util-1.6.22.tgz", "integrity": "sha512-H1rQc1ZOHANWBvPcW+JpGwr+juXSxM8Q8YCkm3GhZd8REu1fHR3z99CErO1p9pkcfcxZnMdIZdIsXkOHY0NilA==" }, + "@monaco-editor/loader": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.4.0.tgz", + "integrity": "sha512-00ioBig0x642hytVspPl7DbQyaSWRaolYie/UFNjoTdvoKPzo6xrXLhTk9ixgIKcLH5b5vDOjVNiGyY+uDCUlg==", + "requires": { + "state-local": "^1.0.6" + } + }, + "@monaco-editor/react": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.6.0.tgz", + "integrity": "sha512-RFkU9/i7cN2bsq/iTkurMWOEErmYcY6JiQI3Jn+WeR/FGISH8JbHERjpS9oRuSOPvDMJI0Z8nJeKkbOs9sBYQw==", + "requires": { + "@monaco-editor/loader": "^1.4.0" + } + }, "@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -26198,14 +26230,6 @@ "@babel/runtime": "^7.10.3" } }, - "react-monaco-editor": { - "version": "0.54.0", - "resolved": "https://registry.npmjs.org/react-monaco-editor/-/react-monaco-editor-0.54.0.tgz", - "integrity": "sha512-9JwO69851mfpuhYLHlKbae7omQWJ/2ICE2lbL0VHyNyZR8rCOH7440u+zAtDgiOMpLwmYdY1sEZCdRefywX6GQ==", - "requires": { - "prop-types": "^15.8.1" - } - }, "react-router": { "version": "5.3.4", "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.4.tgz", @@ -27224,6 +27248,11 @@ "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==" }, + "state-local": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz", + "integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==" + }, "state-toggle": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/state-toggle/-/state-toggle-1.0.3.tgz", diff --git a/package.json b/package.json index 23e79ea0..7ce9db51 100644 --- a/package.json +++ b/package.json @@ -17,9 +17,10 @@ "prettier": "npx prettier --write './sidebars.js' 'docusaurus.config.js' && npm run pretty-source", "pretty-source": "npx prettier --ignore-unknown --write 'src/**/*'", "pretty-docs": "npx prettier --ignore-unknown --write 'docs/**/*'", + "generate-monaco-types": "node scripts/generate-monaco-types.js", "remove-folders": "rm -rf build-docs && rm -rf docs", "clear-folders": "npm run remove-folders && mkdir build-docs && mkdir docs", - "embed-code": "npm run prettier && tsc --module es2015 --target es2015 --moduleResolution node --esModuleInterop true src/examples/*.ts && node scripts/copy-examples.js && npm run clear-folders && cp -r src/docs/* build-docs/ && rm -r docs && mv build-docs docs && npm run pretty-docs" + "embed-code": "npm run generate-monaco-types && npm run prettier && tsc --module es2015 --target es2015 --moduleResolution node --esModuleInterop true src/examples/*.ts && node scripts/copy-examples.js && npm run clear-folders && cp -r src/docs/* build-docs/ && rm -r docs && mv build-docs docs && npm run pretty-docs" }, "dependencies": { "@airgap/beacon-sdk": "4.0.10", @@ -28,14 +29,14 @@ "@docusaurus/preset-classic": "2.4.3", "@docusaurus/theme-live-codeblock": "^2.4.3", "@mdx-js/react": "^1.6.22", + "@monaco-editor/react": "^4.6.0", "@taquito/beacon-wallet": "^17.3.1", "@taquito/taquito": "^17.3.1", "clsx": "^1.2.1", "mermaid": "^10.4.0", "prism-react-renderer": "^1.3.5", "react": "^17.0.2", - "react-dom": "^17.0.2", - "react-monaco-editor": "^0.54.0" + "react-dom": "^17.0.2" }, "devDependencies": { "@docusaurus/module-type-aliases": "2.4.3", diff --git a/scripts/generate-monaco-types.js b/scripts/generate-monaco-types.js index ec3e8a5b..ae2d74c5 100644 --- a/scripts/generate-monaco-types.js +++ b/scripts/generate-monaco-types.js @@ -2,7 +2,7 @@ const fs = require("fs"); const getFilesRecursively = require("./get-files-in-folder"); const files = getFilesRecursively( - "./node_modules/@airgap/beacon-sdk/dist/cjs/" + "./node_modules/@airgap/" ).filter((file) => file.endsWith(".d.ts")); files.push( ...getFilesRecursively("./node_modules/@taquito/").filter((file) => diff --git a/src/components/Monaco.tsx b/src/components/Monaco.tsx new file mode 100644 index 00000000..622fec4c --- /dev/null +++ b/src/components/Monaco.tsx @@ -0,0 +1,66 @@ +import React, { Suspense, lazy } from "react"; +import { useColorMode } from "@docusaurus/theme-common"; +import { libs } from "./monaco-types"; + +import Editor from "@monaco-editor/react"; + +function Monaco(props) { + let monacoRef; + const { colorMode } = useColorMode(); + + function onEditorWillMount(monaco) { + monacoRef = monaco; + const vsDarkTheme = { + base: "vs-dark", + inherit: true, + rules: [{ background: "121212" }], + colors: { + "editor.background": "#121212", + }, + }; + + monaco.editor.defineTheme("vs-dark", vsDarkTheme); + + monaco.languages.typescript.typescriptDefaults.setCompilerOptions({ + target: monaco.languages.typescript.ScriptTarget.ES2017, + allowNonTsExtensions: true, + moduleResolution: monaco.languages.typescript.ModuleResolutionKind.NodeJs, + module: monaco.languages.typescript.ModuleKind.ESNext, + typeRoots: ["node_modules/@types"], + }); + + libs.forEach((lib) => { + const MONACO_LIB_PREFIX = "file:///node_modules/"; + const path = `${MONACO_LIB_PREFIX}${lib.name}`; + monaco.languages.typescript.typescriptDefaults.addExtraLib(lib.dts, path); + }); + + if (props.editorWillMount) { + props.editorWillMount(monaco); + } + } + + function onEditorDidMount(editor) { + editor.setModel( + monacoRef.editor.createModel( + props.value, + props.language, + monacoRef.Uri.parse(`file:///main-${Math.random()}.ts`), + ), + ); + } + + return ( + Loading}> + + + ); +} + +export default Monaco; diff --git a/src/components/RunnableCode.tsx b/src/components/RunnableCode.tsx new file mode 100644 index 00000000..eb3ff731 --- /dev/null +++ b/src/components/RunnableCode.tsx @@ -0,0 +1,165 @@ +import React, { useState } from "react"; +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; +import BrowserWindow from "./BrowserWindow/BrowserWindow"; +import LoadingAnimation from "./LoadingAnimation"; +import { ExecutionState } from "../ExecutionState"; +import BrowserOnly from "@docusaurus/BrowserOnly"; + +const Child = ({ code }) => { + const { DAppClient } = require("../node_modules/beacon-sdk/dist/cjs"); + + const Monaco = require("./Monaco").default; + + const { copyShareUrl, runBeaconCode } = require("../utils"); + + const [runnableCode, setRunnableCode] = useState( + code.props.children.props.children, + ); + const [executionState, setExecutionState] = useState(ExecutionState.INIT); + const [output, setOutput] = useState(""); + const [readonly, setReadonly] = useState(true); + + const execute = async () => { + if (executionState === ExecutionState.STARTED) { + return; + } + await clear(); + setExecutionState(ExecutionState.STARTED); + await runBeaconCode(runnableCode, setOutput); + setExecutionState(ExecutionState.ENDED); + }; + const reset = async () => { + clear(); + const dAppClient = new DAppClient({ name: "Cleanup" }); + await dAppClient.destroy(); + }; + const clear = async () => { + setOutput(""); + setExecutionState(ExecutionState.INIT); + }; + const toggleReadonly = async () => { + setReadonly(!readonly); + }; + const handleShareUrl = async () => { + copyShareUrl(runnableCode); + }; + + const numberOfLines = 1 + runnableCode.split("\n").length; + const editorLayout = { + width: 800, + height: 18 * numberOfLines, + }; + + const setInput = (input: string) => { + setRunnableCode(input); + }; + + return ( + <> + {readonly ? ( + code + ) : ( + + )} + + + + + + + + {executionState !== ExecutionState.INIT ? ( + <> +

Output

+
+              
+                {executionState === ExecutionState.STARTED ? (
+                  <>
+                    
+                  
+                ) : (
+                  ""
+                )}
+                {output || executionState === ExecutionState.ENDED
+                  ? output
+                  : "Waiting for output..."}
+              
+            
+ + ) : ( + "" + )} +
+ + ); +}; + +export const RunnableCode = ({ children, color, beacon, taquito }) => { + return ( + }> + {() => ( + + + + + + + + + )} + + ); +}; diff --git a/src/pages/playground.tsx b/src/pages/playground.tsx new file mode 100644 index 00000000..06dd0a5a --- /dev/null +++ b/src/pages/playground.tsx @@ -0,0 +1,179 @@ +import React, { useState } from "react"; +import classnames from "classnames"; +import Layout from "@theme/Layout"; +import useWindowSize from "@site/src/hooks/useWindowSize"; +import styles from "./styles.module.css"; +import { ExecutionState } from "../ExecutionState"; + +import BrowserOnly from "@docusaurus/BrowserOnly"; +import ErrorBoundary from "@docusaurus/ErrorBoundary"; + +const defaultCode = `import { DAppClient } from "@airgap/beacon-sdk"; + +const dAppClient = new DAppClient({ name: 'Beacon Docs' }) + +const activeAccount = await dAppClient.getActiveAccount() +if (activeAccount) { + // User already has account connected, everything is ready + // You can now do an operation request, sign request, or send another permission request to switch wallet + console.log('Already connected:', activeAccount.address) + return activeAccount +} else { + const permissions = await dAppClient.requestPermissions() + console.log('New connection:', permissions.address) + return permissions +}`; + +function Playground() { + if (typeof window === "undefined") { + return null; + } + + const urlParams = new URLSearchParams(window.location.search); + const initialCode = urlParams.has("code") + ? atob(urlParams.get("code")) + : defaultCode; + + const [input, setInput] = useState(initialCode); + const [output, setOutput] = useState(""); + const [executionState, setExecutionState] = useState(ExecutionState.INIT); + + const windowSize = useWindowSize(); + + const inputChanged = (str) => { + setInput(str); + }; + + const headerLayout = { + height: 100, + }; + + const editorWidthRatio = 3 / 5; + const editorLayout = { + xs: { + width: windowSize.width, + height: 200, + }, + lg: { + width: editorWidthRatio * windowSize.width, + height: windowSize.height - headerLayout.height, + }, + }; + + const outputLayout = { + xs: { + width: windowSize.width, + height: windowSize.height - headerLayout.height - editorLayout.xs.height, + }, + lg: { + width: (1 - editorWidthRatio - 0.05) * windowSize.width, + height: windowSize.height, + }, + }; + + return ( + }> + {() => { + const { DAppClient } = require("@airgap/beacon-sdk"); + const Monaco = require("@site/src/components/Monaco").default; + const { copyShareUrl, runBeaconCode } = require("../utils"); + + const execute = async () => { + if (executionState === ExecutionState.STARTED) { + return; + } + await clear(); + setExecutionState(ExecutionState.STARTED); + await runBeaconCode(input, setOutput); + setExecutionState(ExecutionState.ENDED); + }; + const clear = async () => { + setOutput(""); + setExecutionState(ExecutionState.INIT); + }; + + const handleClickShare = () => { + copyShareUrl(input); + }; + + const reset = async () => { + clear(); + const dAppClient = new DAppClient({ name: "Cleanup" }); + await dAppClient.destroy(); + }; + + return ( + +
+ + + + +
+ +
+ ( +
+

+ This editor crashed because of error: {error.message}. +

+ +
+ )} + > + 600 + ? editorLayout.lg + : editorLayout.xs)} + language="typescript" + value={input} + onChange={inputChanged} + options={{ minimap: { enabled: false }, wordWrap: "on" }} + /> + 600 + ? outputLayout.lg + : outputLayout.xs)} + language="shell" + value={output} + options={{ + readOnly: true, + minimap: { enabled: false }, + wordWrap: "on", + }} + /> +
+
+
+ ); + }} +
+ ); +} + +export default Playground; diff --git a/src/pages/styles.module.css b/src/pages/styles.module.css new file mode 100644 index 00000000..d76a889b --- /dev/null +++ b/src/pages/styles.module.css @@ -0,0 +1,115 @@ +/** + * CSS files with the .module.css suffix will be treated as CSS modules + * and scoped locally. + */ + +li, +p { + max-width: 70ch; +} + +body > div { + overflow: hidden; +} + +.runbox { + margin: 10px; +} + +.headerContainer { + display: flex; + flex-wrap: wrap; + margin-left: 10px; + padding: 10px; +} + +.argsInputContainer { + display: flex; + height: 30px; + margin-right: 10px; +} + +.argsInputField { + color: var(--ifm-navbar-search-input-color); + background-color: var(--ifm-navbar-search-input-background-color); + border: 1px solid var(--ifm-color-emphasis-500); + border-radius: 4px; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + width: 300px; + padding: 10px; + outline: none; +} + +html[data-theme="dark"] .argsInputField { + border: 1px solid var(--ifm-color-emphasis-100); +} + +.argsIconContainer { + padding-top: 4px; + margin-left: 0; + background-color: var(--ifm-navbar-search-input-background-color); + border: 1px solid var(--ifm-color-emphasis-500); + border-left: none; + border-radius: 4px; + border-top-left-radius: 0; + border-bottom-left-radius: 0; + color: var(--ifm-color-primary); + min-width: 50px; + text-align: center; +} + +html[data-theme="dark"] .argsIconContainer { + border: 1px solid var(--ifm-color-emphasis-100); +} + +.argsIconContainer:hover { + cursor: pointer; + background-color: var(--ifm-color-primary); + color: white; +} + +.helperButton { + margin-right: 10px; + height: 30px; + padding-top: 4px; + background-color: var(--ifm-navbar-search-input-background-color); + border: 1px solid var(--ifm-color-emphasis-500); + border-radius: 4px; + color: var(--ifm-color-primary); + text-align: center; + font-size: var(--ifm-font-size-base); + padding: 0px 12px; +} + +.helperButton:hover { + cursor: pointer; + background-color: var(--ifm-color-primary); + color: white; +} + +html[data-theme="dark"] .helperButton { + border: 1px solid var(--ifm-color-emphasis-100); +} + +.row { + display: flex; + flex-direction: row; + flex-wrap: wrap; +} + +.spinner { + animation: rotate 1.5s linear infinite; +} + +@keyframes rotate { + 100% { + transform: rotate(360deg); + } +} + +@media screen and (max-width: 600px) { + .argsInputField { + width: 100%; + } +} diff --git a/src/theme/Playground/index.js b/src/theme/Playground/index.js index aad11c72..722c7a91 100644 --- a/src/theme/Playground/index.js +++ b/src/theme/Playground/index.js @@ -110,6 +110,7 @@ export default function Playground({ children, transformCode, ...props }) { noInline={noInline} transformCode={transformCode ?? ((code) => `${code};`)} theme={prismTheme} + disabled={true} {...props} > {playgroundPosition === "top" ? ( diff --git a/src/utils.ts b/src/utils.ts index 4c19eb20..31dddf31 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,6 +1,113 @@ -import { DAppClient } from "./node_modules/beacon-sdk/dist/cjs"; +import * as beacon from "./node_modules/beacon-sdk/dist/cjs"; +import * as ts from "typescript"; + +import * as taquito from "@taquito/taquito"; +import * as taquitoWallet from "@taquito/beacon-wallet"; + +function replaceAll(string: string, search: string, replace: string) { + return string.split(search).join(replace); +} + +const removeImports = (code: string) => { + const lines = code.split("\n"); + let include = true; + + return lines + .map((l) => { + if (l.trim().startsWith("import")) { + include = false; + } + + const out = include ? l : undefined; + + if (l.indexOf("@airgap/beacon-sdk") >= 0 || l.indexOf("@taquito") >= 0) { + include = true; + } + + return out; + }) + .filter((l) => !!l) + .join("\n"); +}; + +export const runBeaconCode = ( + rawCode: string, + setOutput: (str: string) => void, +) => { + let code = rawCode; + + let output = ""; + const appendOutput = (str: string) => { + output += "\n" + str; + setOutput(output.trim()); + }; + + const myLog = (...args: any[]) => { + console.log("CODE_RUNNER:", ...args); + appendOutput( + args + .map((arg) => + typeof arg === "object" ? JSON.stringify(arg, null, 2) : arg, + ) + .join(" "), + ); + }; + + code = replaceAll(code, "console.log(", "progress("); + code = removeImports(code); + code = ts.transpile(`({ + run: async (beacon: any, taquito: any, taquitoWallet: any, progress: any): string => { + Object.keys(beacon).forEach(key => { + window[key] = beacon[key] + }) + Object.keys(taquito).forEach(key => { + window[key] = taquito[key] + }) + Object.keys(taquitoWallet).forEach(key => { + window[key] = taquitoWallet[key] + }) + return (async () => { + ${code}; + if (typeof result !== 'undefined') { + return result + } + })() + })`); + let runnable: any; + // console.log("TRANSPILED code", code); + return new Promise((resolve) => { + try { + runnable = eval(code); + runnable + .run(beacon, taquito, taquitoWallet, myLog) + .then((result: string) => { + if (result) { + appendOutput("Returned:\n" + JSON.stringify(result, null, 2)); + } + resolve(result); + }) + .catch((err: any) => { + console.warn(err); + appendOutput(JSON.stringify(err, null, 2)); + resolve(err); + }); + } catch (e) { + appendOutput(e); + console.error(e); + resolve(e); + } + }); +}; + +export const copyShareUrl = (input: string) => { + const url = `https://${window.location.host}/playground?code=${btoa(input)}`; + + navigator.clipboard + .writeText(url) + .catch((err) => console.error("Failed to copy to url!", err)); +}; export const reset = async () => { - const dAppClient = new DAppClient({ name: "Cleanup" }); + const dAppClient = new beacon.DAppClient({ name: "Cleanup" }); await dAppClient.destroy(); };