﻿{"id":835486,"date":"2026-02-18T18:56:15","date_gmt":"2026-02-18T18:56:15","guid":{"rendered":"https:\/\/gridnet.org\/wpp\/?p=835486"},"modified":"2026-02-20T07:57:35","modified_gmt":"2026-02-20T07:57:35","slug":"your-first-decentralized-app-in-30-minutes","status":"publish","type":"post","link":"https:\/\/mag.gridnet.org\/index.php\/2026\/02\/18\/your-first-decentralized-app-in-30-minutes\/","title":{"rendered":"Your First Decentralized App in 30 Minutes \u2014 A Step-by-Step GRIDNET OS Tutorial"},"content":{"rendered":"<h2>You Already Know Everything You Need<\/h2>\n<p>Here is the single most important sentence in this entire article: <strong>if you can write HTML, CSS, and JavaScript, you can build a decentralized application for GRIDNET OS right now.<\/strong><\/p>\n<p>No new language. No proprietary framework. No six-month learning curve. You write the same <code>&lt;div&gt;<\/code>s, the same flexbox layouts, the same ES6 classes you have been writing for years \u2014 and your code runs inside a decentralized operating system backed by a blockchain. The blockchain is underneath, but you never touch it directly. You interact with it through <strong>CVMContext<\/strong>: a clean, familiar JavaScript singleton API. You call methods. You register callbacks. You <code>await<\/code> promises. That&#8217;s it.<\/p>\n<p>Even when you venture deeper \u2014 into <strong>GridScript<\/strong>, the native shell of GRIDNET OS \u2014 you will find yourself on familiar ground. GridScript commands mirror the Linux\/DOS commands you already know: <code>cd<\/code>, <code>ls<\/code>, <code>cat<\/code>, <code>rm<\/code>, <code>mkdir<\/code>, <code>touch<\/code>, <code>send<\/code>. The transaction model uses <code>BT<\/code>\/<code>CT<\/code> (Begin Transaction \/ Commit Transaction), which maps directly to <code>BEGIN<\/code>\/<code>COMMIT<\/code> from SQL. Same concept, blockchain-backed.<\/p>\n<p>In the next thirty minutes, you will build a complete, working decentralized application \u2014 a &#8220;Hello Blockchain&#8221; dApp that reads data from and writes data to a real blockchain. Every step includes full, runnable code. No fragments. No &#8220;exercise left to the reader.&#8221; By the end, you will have a <code>.app<\/code> file you can deploy to the decentralized network.<\/p>\n<p>Let&#8217;s begin.<\/p>\n<h2>Prerequisites: What You Need Before We Start<\/h2>\n<p>You need exactly four things:<\/p>\n<ol>\n<li><strong>GRIDNET Core running.<\/strong> This is the full-node software that serves the GRIDNET OS web interface. Download it from <a href=\"https:\/\/gridnet.org\">gridnet.org<\/a> and run it. The node will start a local web server (typically at <code>https:\/\/localhost<\/code>) that hosts the entire decentralized operating system UI.<\/li>\n<li><strong>A GRIDNET OS account (identity).<\/strong> You must be <strong>logged in<\/strong> to write data to the blockchain. If you haven&#8217;t created an account yet, open the GRIDNET OS UI and use the Wallet dApp to generate a new identity. Reading data is public and free, but any write or commit operation requires an authenticated session with a valid cryptographic identity.<\/li>\n<li><strong>A modern browser.<\/strong> Chromium-based (Chrome, Edge, Brave) or Firefox. The GRIDNET OS web interface uses standard Web APIs: Shadow DOM, ES6 modules, WebSocket, WebCrypto.<\/li>\n<li><strong>A text editor.<\/strong> VS Code, Sublime, Notepad++ \u2014 anything you like. You will write a single JavaScript file.<\/li>\n<\/ol>\n<p>That&#8217;s it. No npm. No node_modules. No build toolchain (unless you <em>choose<\/em> to use one for larger projects). For your first dApp, a text editor and a browser are all you need.<\/p>\n<p>Optional but helpful: open the GRIDNET OS UI at <code>https:\/\/ui.gridnet.org<\/code> (the public instance) or your local <code>https:\/\/localhost<\/code> instance. Explore the desktop environment. Open the Terminal dApp and the File Manager. Get a feel for the windowed environment \u2014 because your application will live inside one of these windows.<\/p>\n<h2>Step 1: Create the HTML Skeleton \u2014 Shadow DOM and Scoped CSS<\/h2>\n<figure><img decoding=\"async\" src=\"https:\/\/gridnet.org\/wpp\/wp-content\/uploads\/2026\/02\/your-first-decentralized-app-in-30-minutes-shadow-dom.png\" alt=\"Shadow DOM isolation \u2014 each dApp window is an island\" style=\"width:100%;\" \/><figcaption>Shadow DOM encapsulation: every dApp window is an isolated island of sovereign computation.<\/figcaption><\/figure>\n<p>Every GRIDNET OS UI dApp is a JavaScript ES6 class that extends <code>CWindow<\/code> \u2014 the base class provided by the system at <code>\/lib\/window.js<\/code>. When your dApp is instantiated, <code>CWindow<\/code> creates a dedicated window on the desktop and renders your HTML content inside a <strong>Shadow DOM tree<\/strong>. This Shadow DOM is the key to everything: it isolates your styles, your DOM, and your scripts from every other dApp and from the OS shell itself.<\/p>\n<p>Here is the complete skeleton of a GRIDNET OS dApp:<\/p>\n<pre>\n\"use strict\"\n\nimport { CWindow } from \"\/lib\/window.js\"\n\n\/\/ \u2500\u2500 HTML body for the dApp window \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst myAppBody = `\n&lt;link rel=\"stylesheet\" href=\"\/css\/windowDefault.css\" \/&gt;\n&lt;style&gt;\n  .container {\n    display: flex;\n    flex-direction: column;\n    align-items: center;\n    justify-content: center;\n    height: 100%;\n    width: 100%;\n    padding: 1em;\n    box-sizing: border-box;\n    font-family: 'Rajdhani', sans-serif;\n    color: #22fafc;\n    background: linear-gradient(135deg, #0a0a14, #0d1a2d);\n  }\n  h1 {\n    font-size: 1.8rem;\n    margin-bottom: 0.5em;\n    text-shadow: 0 0 15px rgba(34, 250, 252, 0.4);\n  }\n  .status {\n    font-size: 0.9rem;\n    color: #8892b0;\n  }\n&lt;\/style&gt;\n&lt;div class=\"container\"&gt;\n  &lt;h1 id=\"title\"&gt;Hello Blockchain&lt;\/h1&gt;\n  &lt;div class=\"status\" id=\"status\"&gt;Initializing...&lt;\/div&gt;\n&lt;\/div&gt;\n`;\n\n\/\/ \u2500\u2500 dApp class \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nclass CHelloBlockchain extends CWindow {\n\n  constructor(positionX, positionY, width, height, data, dataType, filePath, thread) {\n    super(positionX, positionY, width, height, myAppBody,\n      \"Hello Blockchain\",       \/\/ window title\n      CHelloBlockchain.getIcon(), \/\/ icon\n      true                       \/\/ enable Shadow DOM\n    );\n\n    \/\/ Set a unique thread ID for this dApp instance.\n    \/\/ Every dApp should set its own thread ID to avoid collisions\n    \/\/ with other running dApps. The pattern is: PREFIX + process ID.\n    this.setThreadID = 'HELLO_BLOCKCHAIN_' + this.getProcessID;\n  }\n\n  static getPackageID() {\n    return \"org.gridnetproject.UIdApps.helloBlockchain\";\n  }\n\n  static getDefaultCategory() {\n    return 'dApps';\n  }\n\n  static getIcon() {\n    return ''; \/\/ Base64 data:image\/png;base64,... goes here\n  }\n\n  open() {\n    super.open();\n    const status = this.getControl('status');\n    status.textContent = 'dApp is running!';\n  }\n\n  closeWindow() {\n    super.closeWindow();\n  }\n}\n\nexport default CHelloBlockchain;\n<\/pre>\n<p>Let&#8217;s break down what&#8217;s happening:<\/p>\n<h3>The HTML Body String<\/h3>\n<p>The <code>myAppBody<\/code> variable is a template literal containing your dApp&#8217;s entire HTML payload. This string is injected into the Shadow DOM when the window is created. Notice several critical patterns:<\/p>\n<ul>\n<li><strong>The standard stylesheet link<\/strong>: <code>&lt;link rel=\"stylesheet\" href=\"\/css\/windowDefault.css\" \/&gt;<\/code> imports the GRIDNET OS default window styles. Always include this.<\/li>\n<li><strong>Scoped <code>&lt;style&gt;<\/code> block<\/strong>: Your CSS lives <em>inside<\/em> the HTML body. Because of Shadow DOM, these styles cannot leak out to other dApps, and no external styles can leak in. You have complete CSS isolation.<\/li>\n<li><strong>Percentage and em\/rem units<\/strong>: Never use <code>vw<\/code> or <code>vh<\/code>. Your dApp does not own the viewport \u2014 it owns a window. Use <code>%<\/code> for layout dimensions relative to the window, and <code>em<\/code>\/<code>rem<\/code> for typography and spacing.<\/li>\n<li><strong>Flexbox\/Grid layouts<\/strong>: Use <code>display: flex<\/code> or <code>display: grid<\/code> with relative units. Your window can be resized, maximised, minimised, or snapped \u2014 your layout must adapt fluidly.<\/li>\n<\/ul>\n<h3>The Class<\/h3>\n<p>Your dApp class <strong>must<\/strong> extend <code>CWindow<\/code>. The <code>CWindow<\/code> base constructor takes four positional parameters: <code>positionX<\/code>, <code>positionY<\/code>, <code>width<\/code>, and <code>height<\/code>. However, the PackageManager may pass <strong>up to eight parameters<\/strong> when instantiating your dApp \u2014 the extra four are provided when the system launches your app in a file-associated context (e.g., the user double-clicks a file that your app handles):<\/p>\n<ul>\n<li><code>positionX<\/code>, <code>positionY<\/code> \u2014 initial window position<\/li>\n<li><code>width<\/code>, <code>height<\/code> \u2014 initial window dimensions<\/li>\n<li><code>data<\/code>, <code>dataType<\/code> \u2014 optional data and its type (provided when opening a file with your app)<\/li>\n<li><code>filePath<\/code> \u2014 optional file path context (the file that was opened)<\/li>\n<li><code>thread<\/code> \u2014 optional thread reference<\/li>\n<\/ul>\n<p>System dApps like Terminal only declare the four core parameters they need. For user-deployed dApps that handle file associations, you should accept all eight. The <code>super()<\/code> call passes the four positional parameters to <code>CWindow<\/code> along with your HTML body, the window title, your icon, and <code>true<\/code> to enable Shadow DOM (which you should <em>always<\/em> enable).<\/p>\n<h3>Key Methods You Must Implement<\/h3>\n<ul>\n<li><code>static getPackageID()<\/code> \u2014 returns a unique reverse-domain identifier. For user-deployable dApps, start with <code>org.gridnetproject.UIdApps.<\/code><\/li>\n<li><code>static getIcon()<\/code> \u2014 returns a Base64-encoded PNG icon string (<code>data:image\/png;base64,...<\/code>). Can be empty during development.<\/li>\n<li><code>open()<\/code> \u2014 called when the window has been created and is visible. This is where you initialise your UI, register event listeners, and start any background processes. Always call <code>super.open()<\/code> first.<\/li>\n<li><code>closeWindow()<\/code> \u2014 called when the user closes the window. Clean up event listeners, stop threads, release resources. Always call <code>super.closeWindow()<\/code>.<\/li>\n<\/ul>\n<h3>Accessing DOM Elements: The Shadow DOM Way<\/h3>\n<p>This is <strong>the most important rule in GRIDNET OS dApp development<\/strong>: never use <code>document.getElementById()<\/code> or <code>document.querySelector()<\/code>. These methods search the main document \u2014 the OS shell \u2014 not your Shadow DOM. They will not find your elements.<\/p>\n<p>Instead, use:<\/p>\n<ul>\n<li><code>this.getControl('elementId')<\/code> \u2014 finds an element by its <code>id<\/code> attribute within your Shadow DOM<\/li>\n<li><code>this.getBody.querySelector('.my-class')<\/code> \u2014 CSS selector scoped to your dApp&#8217;s body<\/li>\n<li><code>this.getBody.querySelectorAll('button')<\/code> \u2014 all matching elements within your Shadow DOM<\/li>\n<\/ul>\n<pre>\n\/\/ \u274c BAD \u2014 searches the main document, finds nothing\nconst btn = document.getElementById('my-button'); \/\/ Returns null!\n\n\/\/ \u2705 GOOD \u2014 searches within your Shadow DOM\nconst btn = this.getControl('my-button');\nconst labels = this.getBody.querySelectorAll('.label');\n<\/pre>\n<h3>CSS Rules \u2014 The Essentials<\/h3>\n<table style=\"width:100%; border-collapse: collapse; margin: 1em 0;\">\n<tr style=\"background: #0d1a2d; color: #ff4444;\">\n<th style=\"padding: 0.5em; border: 1px solid #1a3a5c;\">\u274c Do NOT<\/th>\n<th style=\"padding: 0.5em; border: 1px solid #1a3a5c;\">Why<\/th>\n<th style=\"padding: 0.5em; border: 1px solid #1a3a5c; color: #00ff88;\">\u2705 Do Instead<\/th>\n<\/tr>\n<tr>\n<td style=\"padding: 0.5em; border: 1px solid #1a3a5c;\">Use <code>vw<\/code> or <code>vh<\/code><\/td>\n<td style=\"padding: 0.5em; border: 1px solid #1a3a5c;\">Relative to viewport, not your window<\/td>\n<td style=\"padding: 0.5em; border: 1px solid #1a3a5c;\">Use <code>%<\/code>, <code>em<\/code>, <code>rem<\/code><\/td>\n<\/tr>\n<tr>\n<td style=\"padding: 0.5em; border: 1px solid #1a3a5c;\">Use <code>position: fixed<\/code><\/td>\n<td style=\"padding: 0.5em; border: 1px solid #1a3a5c;\">Positions relative to viewport<\/td>\n<td style=\"padding: 0.5em; border: 1px solid #1a3a5c;\">Use <code>relative<\/code>\/<code>absolute<\/code> within your container<\/td>\n<\/tr>\n<tr>\n<td style=\"padding: 0.5em; border: 1px solid #1a3a5c;\">Use external CDN stylesheets<\/td>\n<td style=\"padding: 0.5em; border: 1px solid #1a3a5c;\">Not available inside Shadow DOM<\/td>\n<td style=\"padding: 0.5em; border: 1px solid #1a3a5c;\">Inline all CSS in your body string<\/td>\n<\/tr>\n<tr>\n<td style=\"padding: 0.5em; border: 1px solid #1a3a5c;\">Overuse <code>!important<\/code><\/td>\n<td style=\"padding: 0.5em; border: 1px solid #1a3a5c;\">Breaks cascade, hard to debug<\/td>\n<td style=\"padding: 0.5em; border: 1px solid #1a3a5c;\">Use specific selectors<\/td>\n<\/tr>\n<\/table>\n<h2>Step 2: Connect to the Blockchain via the CVMContext API<\/h2>\n<figure><img decoding=\"async\" src=\"https:\/\/gridnet.org\/wpp\/wp-content\/uploads\/2026\/02\/your-first-decentralized-app-in-30-minutes-cvmcontext-api.png\" alt=\"CVMContext \u2014 the JavaScript gateway to the decentralized stack\" style=\"width:100%;\" \/><figcaption>CVMContext: your single point of entry to the entire GRIDNET OS decentralized stack.<\/figcaption><\/figure>\n<p><code>CVMContext<\/code> is the singleton JavaScript object that bridges your dApp to the entire GRIDNET OS backend \u2014 blockchain, decentralized file system, networking, thread management, user identity, and more. It lives at <code>\/lib\/VMContext.js<\/code> and you access it with a single call:<\/p>\n<pre>\nconst vmContext = CVMContext.getInstance();\n<\/pre>\n<p>That&#8217;s your gateway to the decentralized world. Let&#8217;s connect our Hello Blockchain dApp to it.<\/p>\n<h3>The Event-Driven Architecture<\/h3>\n<p>GRIDNET OS communication is <strong>event-driven<\/strong>. You don&#8217;t poll the blockchain. You register listener callbacks and the system notifies you when events occur. Here are the key listener types you&#8217;ll use:<\/p>\n<p><!-- Architecture SVG Diagram --><\/p>\n<p>Let&#8217;s upgrade our Hello Blockchain dApp to connect to the blockchain and monitor its state:<\/p>\n<pre>\n\"use strict\"\n\nimport { CWindow } from \"\/lib\/window.js\"\nimport { CVMMetaSection, CVMMetaEntry, CVMMetaGenerator, CVMMetaParser }\n  from '\/lib\/MetaData.js'\nimport { CTools } from '\/lib\/tools.js'\n\nconst myAppBody = `\n&lt;link rel=\"stylesheet\" href=\"\/css\/windowDefault.css\" \/&gt;\n&lt;style&gt;\n  .container {\n    display: flex;\n    flex-direction: column;\n    height: 100%;\n    width: 100%;\n    padding: 1.5em;\n    box-sizing: border-box;\n    font-family: 'Rajdhani', sans-serif;\n    color: #22fafc;\n    background: linear-gradient(135deg, #0a0a14, #0d1a2d);\n    overflow-y: auto;\n  }\n  h1 {\n    font-size: 1.6rem;\n    margin: 0 0 0.5em 0;\n    text-shadow: 0 0 15px rgba(34, 250, 252, 0.4);\n  }\n  .status-bar {\n    display: flex;\n    gap: 1em;\n    margin-bottom: 1em;\n    flex-wrap: wrap;\n  }\n  .status-item {\n    display: flex;\n    align-items: center;\n    gap: 0.4em;\n    font-size: 0.85rem;\n  }\n  .dot {\n    width: 0.6em;\n    height: 0.6em;\n    border-radius: 50%;\n    background: #ff4444;\n    transition: background 0.3s;\n  }\n  .dot.connected { background: #00ff88; box-shadow: 0 0 6px #00ff88; }\n  .dot.vm-ready { background: #ffd700; box-shadow: 0 0 6px #ffd700; }\n  .log {\n    flex: 1;\n    background: #0a0e1a;\n    border: 1px solid #1a3a5c;\n    border-radius: 6px;\n    padding: 0.8em;\n    font-family: monospace;\n    font-size: 0.8rem;\n    color: #8892b0;\n    overflow-y: auto;\n    white-space: pre-wrap;\n    word-break: break-all;\n  }\n&lt;\/style&gt;\n&lt;div class=\"container\"&gt;\n  &lt;h1&gt;\ud83d\udd17 Hello Blockchain&lt;\/h1&gt;\n  &lt;div class=\"status-bar\"&gt;\n    &lt;div class=\"status-item\"&gt;\n      &lt;div class=\"dot\" id=\"connDot\"&gt;&lt;\/div&gt;\n      &lt;span id=\"connStatus\"&gt;Disconnected&lt;\/span&gt;\n    &lt;\/div&gt;\n    &lt;div class=\"status-item\"&gt;\n      &lt;div class=\"dot\" id=\"vmDot\"&gt;&lt;\/div&gt;\n      &lt;span id=\"vmStatus\"&gt;VM: Initializing&lt;\/span&gt;\n    &lt;\/div&gt;\n  &lt;\/div&gt;\n  &lt;div class=\"log\" id=\"logArea\"&gt;Waiting for connection...&lt;\/div&gt;\n&lt;\/div&gt;\n`;\n\nclass CHelloBlockchain extends CWindow {\n\n  constructor(positionX, positionY, width, height, data, dataType, filePath, thread) {\n    super(positionX, positionY, width, height, myAppBody,\n      \"Hello Blockchain\", CHelloBlockchain.getIcon(), true);\n\n    this.mTools = CTools.getInstance();\n    this.mMetaParser = new CVMMetaParser();\n\n    \/\/ Set a unique thread ID for this dApp instance\n    this.setThreadID = 'HELLO_BLOCKCHAIN_' + this.getProcessID;\n\n    \/\/ Register event listeners with CVMContext\n    \/\/ The second parameter (this.mID) ties the listener to this window instance\n    \/\/ so it can be automatically cleaned up when the window closes.\n    CVMContext.getInstance().addConnectionStatusChangedListener(\n      this.onConnectionChanged.bind(this), this.mID);\n    CVMContext.getInstance().addVMStateChangedListener(\n      this.onVMStateChanged.bind(this), this.mID);\n    CVMContext.getInstance().addNewDFSMsgListener(\n      this.onDFSMessage.bind(this), this.mID);\n    CVMContext.getInstance().addVMMetaDataListener(\n      this.onVMMetaData.bind(this), this.mID);\n    CVMContext.getInstance().addNewGridScriptResultListener(\n      this.onGridScriptResult.bind(this), this.mID);\n  }\n\n  static getPackageID() {\n    return \"org.gridnetproject.UIdApps.helloBlockchain\";\n  }\n\n  static getDefaultCategory() { return 'dApps'; }\n  static getIcon() { return ''; }\n\n  \/\/ \u2500\u2500 Lifecycle \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n  open() {\n    super.open();\n    this.log('Hello Blockchain dApp started.');\n    this.log('Waiting for connection to GRIDNET Core...');\n\n    \/\/ Check if already connected\n    if (CVMContext.getInstance().getConnectionState == eConnectionState.connected) {\n      this.onConnectionChanged({ state: eConnectionState.connected });\n    }\n  }\n\n  closeWindow() {\n    \/\/ Listeners registered with this.mID are auto-cleaned by CWindow\n    super.closeWindow();\n  }\n\n  \/\/ \u2500\u2500 Event Callbacks \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n  onConnectionChanged(event) {\n    const dot = this.getControl('connDot');\n    const label = this.getControl('connStatus');\n\n    if (event.state == eConnectionState.connected) {\n      dot.classList.add('connected');\n      label.textContent = 'Connected';\n      this.log('\u2705 Connected to GRIDNET Core!');\n    } else if (event.state == eConnectionState.connecting) {\n      label.textContent = 'Connecting...';\n      this.log('\ud83d\udd04 Connecting...');\n    } else {\n      dot.classList.remove('connected');\n      label.textContent = 'Disconnected';\n      this.log('\u274c Disconnected from node.');\n    }\n  }\n\n  onVMStateChanged(event) {\n    const dot = this.getControl('vmDot');\n    const label = this.getControl('vmStatus');\n\n    if (event.state == eVMState.ready || event.state == eVMState.synced) {\n      dot.classList.add('vm-ready');\n      label.textContent = 'VM: Ready';\n      this.log('\u2705 Decentralized VM is ready!');\n    } else if (event.state == eVMState.initializing) {\n      label.textContent = 'VM: Initializing...';\n      this.log('\u23f3 VM initializing...');\n    }\n  }\n\n  onDFSMessage(dfsMsg) {\n    \/\/ Check if this message is for us\n    if (!this.hasNetworkRequestID(dfsMsg.getReqID)) return;\n    this.log('\ud83d\udcc2 DFS response received (reqID: ' + dfsMsg.getReqID + ')');\n  }\n\n  onVMMetaData(metaMsg) {\n    if (!this.hasNetworkRequestID(metaMsg.getReqID)) return;\n    this.log('\ud83d\udce1 VMMetaData received (reqID: ' + metaMsg.getReqID + ')');\n  }\n\n  onGridScriptResult(result) {\n    if (result == null) return;\n    this.log('\u26a1 GridScript result received.');\n  }\n\n  \/\/ \u2500\u2500 Utility \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n  log(message) {\n    const area = this.getControl('logArea');\n    if (!area) return;\n    const timestamp = new Date().toLocaleTimeString();\n    area.textContent += '\\n[' + timestamp + '] ' + message;\n    area.scrollTop = area.scrollHeight;\n  }\n}\n\nexport default CHelloBlockchain;\n<\/pre>\n<p>This version demonstrates the core communication pattern:<\/p>\n<ol>\n<li><strong>Register listeners in the constructor<\/strong> \u2014 always with <code>.bind(this)<\/code> and <code>this.mID<\/code><\/li>\n<li><strong>React to events in callbacks<\/strong> \u2014 connection state changes, VM state changes, DFS messages, meta-data<\/li>\n<li><strong>Check request ownership<\/strong> \u2014 use <code>this.hasNetworkRequestID(msg.getReqID)<\/code> to verify that incoming data is the result of <em>your<\/em> query, not another dApp&#8217;s<\/li>\n<li><strong>Clean up in closeWindow()<\/strong> \u2014 listeners registered with <code>this.mID<\/code> are automatically cleaned up by <code>CWindow<\/code><\/li>\n<\/ol>\n<h2>Step 3: Read Data \u2014 Navigate Domains, List Files, Read State<\/h2>\n<figure><img decoding=\"async\" src=\"https:\/\/gridnet.org\/wpp\/wp-content\/uploads\/2026\/02\/your-first-decentralized-app-in-30-minutes-reading-data.png\" alt=\"Navigating the decentralized file system\" style=\"width:100%;\" \/><figcaption>The Decentralized File System: navigate domains, list files, read state \u2014 all through familiar commands.<\/figcaption><\/figure>\n<p>The GRIDNET OS Decentralized File System (DFS) is how data is stored on-chain. Every account (called a <strong>State Domain<\/strong>) has its own file system \u2014 directories, files, and metadata \u2014 all stored in Merkle Patricia Tries on the blockchain. Navigating this file system uses commands that will feel instantly familiar:<\/p>\n<ul>\n<li><code>doCD(path)<\/code> \u2014 change directory (equivalent to <code>cd<\/code>)<\/li>\n<li><code>doLS()<\/code> \u2014 list directory contents (equivalent to <code>ls<\/code>)<\/li>\n<li><code>doGetFile(path)<\/code> \u2014 read a file (equivalent to <code>cat<\/code>)<\/li>\n<li><code>doNewDir(path)<\/code> \u2014 create directory (equivalent to <code>mkdir<\/code>)<\/li>\n<li><code>doNewFile(path, content)<\/code> \u2014 create\/write a file (equivalent to <code>touch<\/code> + <code>echo &gt;<\/code>)<\/li>\n<li><code>doCommit()<\/code> \u2014 commit pending changes to the blockchain<\/li>\n<li><code>doSync()<\/code> \u2014 synchronize with the latest blockchain state<\/li>\n<\/ul>\n<p>All file system operations go through <code>CVMContext.getInstance().getFileSystem<\/code>, which returns a <code>CFileSystem<\/code> singleton. Here&#8217;s how to read data:<\/p>\n<pre>\n\/\/ Navigate to the root of a state domain and list its contents\nreadBlockchainData() {\n  const fs = CVMContext.getInstance().getFileSystem;\n\n  \/\/ Step 1: Navigate to root directory\n  \/\/ doCD returns an operation status with a request ID\n  let cdResult = fs.doCD('\/', true, false, false, this.getThreadID);\n\n  \/\/ Track the request ID so we can identify the response\n  this.addNetworkRequestID(cdResult.getReqID);\n\n  this.log('\ud83d\udcc2 Navigating to root directory...');\n\n  \/\/ The doCD with doLS=true also triggers a directory listing.\n  \/\/ The results arrive asynchronously in our onDFSMessage callback.\n}\n<\/pre>\n<p>The response arrives in your <code>onDFSMessage<\/code> callback. Here&#8217;s how to parse a directory listing:<\/p>\n<pre>\nonDFSMessage(dfsMsg) {\n  \/\/ Verify this message is for us\n  if (!this.hasNetworkRequestID(dfsMsg.getReqID)) return;\n\n  if (dfsMsg.getData1.byteLength > 0) {\n    let metaData = this.mMetaParser.parse(dfsMsg.getData1);\n\n    if (metaData != 0) {\n      let sections = this.mMetaParser.getSections;\n\n      for (let i = 0; i &lt; sections.length; i++) {\n        let sType = sections[i].getType;\n\n        \/\/ Directory listing response\n        if (sType == eVMMetaSectionType.fileContents) {\n          let entries = sections[i].getEntries;\n\n          for (let a = 0; a &lt; entries.length; a++) {\n            let dataFields = entries[a].getFields;\n            let entryType = entries[a].getType;\n\n            if (entryType == eDFSElementType.directoryEntry) {\n              let dirName = CTools.getInstance().arrayBufferToString(dataFields[1]);\n              this.log('\ud83d\udcc1 Directory: ' + dirName);\n            }\n            else if (entryType == eDFSElementType.fileEntry) {\n              let fileName = CTools.getInstance().arrayBufferToString(dataFields[1]);\n              this.log('\ud83d\udcc4 File: ' + fileName);\n            }\n            else if (entryType == eDFSElementType.stateDomainEntry) {\n              let domainName = CTools.getInstance().arrayBufferToString(dataFields[1]);\n              this.log('\ud83c\udf10 State Domain: ' + domainName);\n            }\n            else if (entryType == eDFSElementType.fileContent) {\n              let fileName = CTools.getInstance().arrayBufferToString(dataFields[1]);\n              let content = dataFields[2];\n              let dataType = dataFields[0];\n\n              \/\/ Convert content based on type\n              let value;\n              switch (dataType) {\n                case eDataType.bytes:\n                  value = CTools.getInstance().arrayBufferToString(content);\n                  break;\n                case eDataType.unsignedInteger:\n                  value = CTools.getInstance().arrayBufferToNumber(content);\n                  break;\n                case eDataType.signedInteger:\n                  value = CTools.getInstance().arrayBufferToNumber(content);\n                  break;\n                default:\n                  value = '[binary data]';\n              }\n              this.log('\ud83d\udcc4 File: ' + fileName + ' = ' + value);\n            }\n          }\n        }\n      }\n    }\n  }\n}\n<\/pre>\n<h3>Thread IDs: Isolating Your dApp&#8217;s Communication<\/h3>\n<p>Every dApp instance needs its own <strong>thread ID<\/strong> \u2014 a unique identifier that tags all outgoing requests so the system can route responses back to the correct window. You set this in the constructor with:<\/p>\n<pre>\nthis.setThreadID = 'HELLO_BLOCKCHAIN_' + this.getProcessID;\n<\/pre>\n<p>The pattern is a descriptive prefix plus <code>this.getProcessID<\/code> (a unique number assigned to each running dApp instance). Once set, you pass <code>this.getThreadID<\/code> as the thread parameter to all DFS method calls. This is how the system&#8217;s built-in dApps work \u2014 for example, Terminal.js uses <code>'XTERM_THREAD_' + this.getProcessID<\/code> and FileManager uses <code>window.getThreadID<\/code> throughout.<\/p>\n<p>Without a unique thread ID, responses from the blockchain could be misrouted to other dApp instances, causing data corruption or missed callbacks.<\/p>\n<h3>Reading a Specific File<\/h3>\n<pre>\nreadFile(filePath) {\n  const fs = CVMContext.getInstance().getFileSystem;\n  let result = fs.doGetFile(filePath, false, this.getThreadID);\n  this.addNetworkRequestID(result.getReqID);\n  this.log('\ud83d\udcd6 Reading file: ' + filePath);\n  \/\/ Response arrives in onDFSMessage callback\n}\n<\/pre>\n<h2>Step 4: Write Data \u2014 BT\/CT Transactions and GridScript Basics<\/h2>\n<figure><img decoding=\"async\" src=\"https:\/\/gridnet.org\/wpp\/wp-content\/uploads\/2026\/02\/your-first-decentralized-app-in-30-minutes-writing-data.png\" alt=\"Writing data to the blockchain via transactions\" style=\"width:100%;\" \/><figcaption>BT\/CT \u2014 Begin Transaction \/ Commit Transaction: the SQL-inspired model for blockchain writes.<\/figcaption><\/figure>\n<p>Reading data is free and instant. <strong>Writing data requires a transaction.<\/strong> This is the fundamental difference between a centralised application and a decentralised one: every state change must be cryptographically signed, validated by the network, and recorded immutably on the blockchain.<\/p>\n<p>The GRIDNET OS transaction model uses two concepts that map directly to database transactions:<\/p>\n<ul>\n<li><code>BT<\/code> (Begin Transaction) \u2014 opens a transaction block. All subsequent state changes are staged.<\/li>\n<li><code>CT<\/code> (Commit Transaction) \u2014 finalises the transaction and submits it to the blockchain for consensus.<\/li>\n<\/ul>\n<p>Between <code>BT<\/code> and <code>CT<\/code>, you can perform file system operations \u2014 create files, write data, create directories \u2014 and they will all be atomically committed as a single blockchain transaction.<\/p>\n<h3>Writing Files to the Blockchain<\/h3>\n<p>The simplest way to write data is through the DFS API:<\/p>\n<pre>\nwriteMessage(message) {\n  const fs = CVMContext.getInstance().getFileSystem;\n\n  \/\/ Create or update a file in the current directory\n  let result = fs.doNewFile('hello.txt', message, false, this.getThreadID);\n  this.addNetworkRequestID(result.getReqID);\n  this.log('\u270d\ufe0f Writing message to hello.txt...');\n\n  \/\/ The file is now staged in the local state.\n  \/\/ To persist it to the blockchain, we need to COMMIT.\n}\n<\/pre>\n<h3>Committing Changes to the Blockchain<\/h3>\n<p>The commit operation is how staged changes become permanent on-chain state. When you commit, GRIDNET Core packages all your pending changes into a blockchain transaction, has it signed (via QR code on your mobile wallet or local keychain), and submits it to the network for consensus.<\/p>\n<pre>\ncommitChanges() {\n  const fs = CVMContext.getInstance().getFileSystem;\n\n  \/\/ Request a commit \u2014 this triggers the signing workflow\n  let result = fs.doCommit();\n  this.addNetworkRequestID(result.getReqID);\n  this.log('\ud83d\udd10 Commit requested. Awaiting authorization...');\n\n  \/\/ The commit flow:\n  \/\/ 1. doCommit() sends a commit request to GRIDNET Core\n  \/\/ 2. Core prepares the transaction and requests signing\n  \/\/ 3. User authorizes (via QR code \/ mobile wallet \/ local keychain)\n  \/\/ 4. Signed transaction is broadcast to the network\n  \/\/ 5. Network validates and includes in next data block\n  \/\/ 6. Your onDFSMessage callback receives confirmation\n}\n<\/pre>\n<p>You can monitor commit state through the <code>VMCommitStateChanged<\/code> listener:<\/p>\n<pre>\n\/\/ In constructor:\nCVMContext.getInstance().addVMCommitStateChangedListener(\n  this.onCommitStateChanged.bind(this), this.mID);\n\n\/\/ Callback:\nonCommitStateChanged(state) {\n  switch (state) {\n    case eCommitState.prePending:\n      this.log('\ud83d\udd12 Commit pre-locked (preparing)...');\n      break;\n    case eCommitState.pending:\n      this.log('\u23f3 Commit pending (awaiting consensus)...');\n      break;\n    case eCommitState.success:\n      this.log('\u2705 Commit successful! Data is now on-chain!');\n      break;\n    case eCommitState.aborted:\n      this.log('\u274c Commit aborted.');\n      break;\n  }\n}\n<\/pre>\n<h3>GridScript: The Shell Beneath<\/h3>\n<p>For more advanced operations \u2014 transferring tokens, executing smart contracts, managing permissions \u2014 you use <strong>GridScript<\/strong>, the stack-based programming language native to GRIDNET OS. GridScript commands are executed through the Terminal dApp or programmatically through your dApp.<\/p>\n<p>Here are the most common GridScript commands you&#8217;ll encounter:<\/p>\n<pre>\n# Navigation (same as Linux\/DOS)\ncd \/                    # Change to root directory\nls                      # List directory contents\ncd myDomain             # Enter a state domain\n\n# File operations\ncat hello.txt           # Read file contents\ntouch newfile.txt       # Create empty file\nmkdir myFolder          # Create directory\nrm oldfile.txt          # Delete file\n\n# Token transfers\nsend recipientAddress 100   # Send 100 GNC tokens\n\n# Transactions\nBT                      # Begin Transaction\n  send alice 50         # Stage a transfer\n  touch receipt.txt     # Stage a file creation\nCT                      # Commit Transaction (atomic)\n\n# Permissions (ACL)\nsetfacl user:bob:rw myfile.txt    # Grant read\/write\ngetfacl myfile.txt                # View permissions\nchown bob myfile.txt              # Change ownership\n<\/pre>\n<p>The <code>BT<\/code>\/<code>CT<\/code> model is powerful because everything between them is <strong>atomic<\/strong> \u2014 either all operations succeed, or none of them do. This is the same guarantee SQL databases provide with <code>BEGIN<\/code>\/<code>COMMIT<\/code>, but backed by decentralised consensus instead of a central server.<\/p>\n<h2>Step 5: Deploy On-Chain \u2014 The .app Format and DFS Commit<\/h2>\n<figure><img decoding=\"async\" src=\"https:\/\/gridnet.org\/wpp\/wp-content\/uploads\/2026\/02\/your-first-decentralized-app-in-30-minutes-deploy.png\" alt=\"Deploying your dApp to the decentralized network\" style=\"width:100%;\" \/><figcaption>From local file to globally available decentralized application \u2014 the deployment pipeline.<\/figcaption><\/figure>\n<p>Deploying your dApp to GRIDNET OS is remarkably simple. There is no app store, no review process, no gatekeepers. You deploy directly to the decentralised file system, and your application becomes available to anyone running a GRIDNET Core node.<\/p>\n<h3>Step 5.1: Prepare Your .app File<\/h3>\n<p>A <code>.app<\/code> file is simply your JavaScript file with its extension changed. That&#8217;s it. No compilation, no transpilation (unless you choose to bundle multiple source files together).<\/p>\n<pre>\n# If your file is HelloBlockchain.js, simply rename it:\n# HelloBlockchain.js \u2192 HelloBlockchain.app\n<\/pre>\n<p>For larger projects with multiple source files, use a bundler like Rollup, Webpack, or Parcel to combine everything into a single file before renaming. Remember: you can freely <code>import<\/code> system-provided libraries (anything under <code>\/lib\/<\/code>) \u2014 those are available at runtime. Only your own code and non-system dependencies need to be in the <code>.app<\/code> file.<\/p>\n<h3>Step 5.2: Upload to the GRIDNET OS Desktop<\/h3>\n<ol>\n<li>Open the GRIDNET OS UI in your browser (<code>https:\/\/ui.gridnet.org<\/code> or your local instance)<\/li>\n<li>Open the <strong>File Manager<\/strong> dApp<\/li>\n<li><strong>Drag and drop<\/strong> your <code>.app<\/code> file onto the File Manager window or directly onto the Desktop<\/li>\n<li>The PackageManager will analyse your file \u2014 watch the log pane at the bottom of the screen for events<\/li>\n<li>Once analysed, your dApp&#8217;s icon appears on the Desktop. Click it to run!<\/li>\n<\/ol>\n<p>At this point, your dApp runs in a <strong>local sandbox<\/strong> \u2014 it&#8217;s loaded from your local upload but not yet persisted to the blockchain.<\/p>\n<h3>Step 5.3: Commit to the Decentralized Network<\/h3>\n<p>To make your dApp permanent and globally available:<\/p>\n<ol>\n<li>In the File Manager, locate your uploaded <code>.app<\/code> file<\/li>\n<li>Click the <strong>\u22ee\u22ee\u22ee Magic Button<\/strong> (the three-dot commit button) to initiate a DFS commit<\/li>\n<li>Authorize the transaction (via QR code on your mobile wallet or local keychain)<\/li>\n<li>Once the commit is confirmed by the network, your dApp is <strong>permanently stored on-chain<\/strong><\/li>\n<\/ol>\n<p>After committing, anyone running a GRIDNET Core node can navigate to your state domain, find your <code>.app<\/code> file, and run your decentralized application. No servers. No hosting fees (beyond the one-time GNC storage cost). No single point of failure.<\/p>\n<h2>The Complete Working Example: &#8220;Hello Blockchain&#8221; \u2014 Read and Write to the Chain<\/h2>\n<figure><img decoding=\"async\" src=\"https:\/\/gridnet.org\/wpp\/wp-content\/uploads\/2026\/02\/your-first-decentralized-app-in-30-minutes-complete-app.png\" alt=\"The complete Hello Blockchain dApp\" style=\"width:100%;\" \/><figcaption>The complete Hello Blockchain dApp \u2014 reading and writing messages to a decentralized blockchain.<\/figcaption><\/figure>\n<p>Here is the <strong>complete, runnable dApp<\/strong> that ties everything together. This application lets you write a message to the blockchain and read it back \u2014 a full round-trip through the decentralised stack. Copy this entire file, save it as <code>HelloBlockchain.app<\/code>, and deploy it.<\/p>\n<pre>\n\"use strict\"\n\n\/\/ \u2500\u2500 Imports \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nimport { CWindow } from \"\/lib\/window.js\"\nimport { CVMMetaSection, CVMMetaEntry, CVMMetaGenerator, CVMMetaParser }\n  from '\/lib\/MetaData.js'\nimport { CTools, CDataConcatenator } from '\/lib\/tools.js'\n\n\/\/ \u2500\u2500 HTML Body \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst helloBody = `\n&lt;link rel=\"stylesheet\" href=\"\/css\/windowDefault.css\" \/&gt;\n&lt;style&gt;\n  * { box-sizing: border-box; margin: 0; padding: 0; }\n\n  .app-container {\n    display: flex;\n    flex-direction: column;\n    height: 100%;\n    width: 100%;\n    padding: 1.5em;\n    font-family: 'Rajdhani', sans-serif;\n    color: #22fafc;\n    background: linear-gradient(135deg, #0a0a14 0%, #0d1a2d 50%, #0a1628 100%);\n    overflow-y: auto;\n  }\n\n  .app-header {\n    display: flex;\n    align-items: center;\n    gap: 0.8em;\n    margin-bottom: 1.2em;\n  }\n\n  .app-header h1 {\n    font-size: 1.5rem;\n    text-shadow: 0 0 12px rgba(34, 250, 252, 0.4);\n  }\n\n  \/* \u2500\u2500 Status Bar \u2500\u2500 *\/\n  .status-bar {\n    display: flex;\n    gap: 1.2em;\n    margin-bottom: 1.2em;\n    flex-wrap: wrap;\n  }\n\n  .status-item {\n    display: flex;\n    align-items: center;\n    gap: 0.3em;\n    font-size: 0.8rem;\n    color: #8892b0;\n  }\n\n  .dot {\n    width: 0.5em; height: 0.5em;\n    border-radius: 50%;\n    background: #ff4444;\n    transition: all 0.3s;\n  }\n  .dot.ok { background: #00ff88; box-shadow: 0 0 6px #00ff88; }\n\n  \/* \u2500\u2500 Sections \u2500\u2500 *\/\n  .section {\n    background: rgba(10, 22, 40, 0.6);\n    border: 1px solid #1a3a5c;\n    border-radius: 8px;\n    padding: 1em;\n    margin-bottom: 1em;\n  }\n  .section h2 {\n    font-size: 1rem;\n    color: #ffd700;\n    margin-bottom: 0.6em;\n    text-transform: uppercase;\n    letter-spacing: 1px;\n  }\n\n  \/* \u2500\u2500 Input Row \u2500\u2500 *\/\n  .input-row {\n    display: flex;\n    gap: 0.6em;\n  }\n  .input-row input {\n    flex: 1;\n    padding: 0.5em 0.8em;\n    border: 1px solid #1a3a5c;\n    border-radius: 4px;\n    background: #0a0e1a;\n    color: #22fafc;\n    font-family: 'Rajdhani', sans-serif;\n    font-size: 0.9rem;\n    outline: none;\n    transition: border-color 0.3s;\n  }\n  .input-row input:focus {\n    border-color: #00f0ff;\n    box-shadow: 0 0 8px rgba(0, 240, 255, 0.2);\n  }\n\n  \/* \u2500\u2500 Buttons \u2500\u2500 *\/\n  .btn {\n    padding: 0.5em 1.2em;\n    border: 1px solid #00f0ff;\n    border-radius: 4px;\n    background: rgba(0, 240, 255, 0.1);\n    color: #00f0ff;\n    font-family: 'Rajdhani', sans-serif;\n    font-size: 0.85rem;\n    font-weight: bold;\n    text-transform: uppercase;\n    letter-spacing: 1px;\n    cursor: pointer;\n    transition: all 0.3s;\n    white-space: nowrap;\n  }\n  .btn:hover {\n    background: rgba(0, 240, 255, 0.25);\n    box-shadow: 0 0 12px rgba(0, 240, 255, 0.3);\n  }\n  .btn.gold {\n    border-color: #ffd700;\n    color: #ffd700;\n    background: rgba(255, 215, 0, 0.1);\n  }\n  .btn.gold:hover {\n    background: rgba(255, 215, 0, 0.25);\n    box-shadow: 0 0 12px rgba(255, 215, 0, 0.3);\n  }\n  .btn.green {\n    border-color: #00ff88;\n    color: #00ff88;\n    background: rgba(0, 255, 136, 0.1);\n  }\n  .btn.green:hover {\n    background: rgba(0, 255, 136, 0.25);\n    box-shadow: 0 0 12px rgba(0, 255, 136, 0.3);\n  }\n\n  \/* \u2500\u2500 Message Display \u2500\u2500 *\/\n  .message-display {\n    padding: 0.8em;\n    background: #0a0e1a;\n    border: 1px dashed #1a3a5c;\n    border-radius: 4px;\n    font-family: monospace;\n    font-size: 0.9rem;\n    color: #00ff88;\n    min-height: 2.5em;\n    word-break: break-all;\n  }\n\n  \/* \u2500\u2500 Log \u2500\u2500 *\/\n  .log-area {\n    flex: 1;\n    min-height: 6em;\n    background: #0a0e1a;\n    border: 1px solid #1a3a5c;\n    border-radius: 6px;\n    padding: 0.6em;\n    font-family: monospace;\n    font-size: 0.75rem;\n    color: #556688;\n    overflow-y: auto;\n    white-space: pre-wrap;\n    word-break: break-all;\n  }\n&lt;\/style&gt;\n\n&lt;div class=\"app-container\"&gt;\n  &lt;div class=\"app-header\"&gt;\n    &lt;h1&gt;\ud83d\udd17 Hello Blockchain&lt;\/h1&gt;\n  &lt;\/div&gt;\n\n  &lt;div class=\"status-bar\"&gt;\n    &lt;div class=\"status-item\"&gt;\n      &lt;div class=\"dot\" id=\"dotConn\"&gt;&lt;\/div&gt; &lt;span id=\"lblConn\"&gt;Disconnected&lt;\/span&gt;\n    &lt;\/div&gt;\n    &lt;div class=\"status-item\"&gt;\n      &lt;div class=\"dot\" id=\"dotVM\"&gt;&lt;\/div&gt; &lt;span id=\"lblVM\"&gt;VM: \u2014&lt;\/span&gt;\n    &lt;\/div&gt;\n    &lt;div class=\"status-item\"&gt;\n      &lt;div class=\"dot\" id=\"dotCommit\"&gt;&lt;\/div&gt; &lt;span id=\"lblCommit\"&gt;Commit: Idle&lt;\/span&gt;\n    &lt;\/div&gt;\n  &lt;\/div&gt;\n\n  &lt;!-- Write Section --&gt;\n  &lt;div class=\"section\"&gt;\n    &lt;h2&gt;\u270d\ufe0f Write to Blockchain&lt;\/h2&gt;\n    &lt;div class=\"input-row\"&gt;\n      &lt;input type=\"text\" id=\"msgInput\" placeholder=\"Type your message here...\" \/&gt;\n      &lt;button class=\"btn gold\" id=\"btnWrite\"&gt;Write&lt;\/button&gt;\n      &lt;button class=\"btn green\" id=\"btnCommit\"&gt;Commit&lt;\/button&gt;\n    &lt;\/div&gt;\n  &lt;\/div&gt;\n\n  &lt;!-- Read Section --&gt;\n  &lt;div class=\"section\"&gt;\n    &lt;h2&gt;\ud83d\udcd6 Read from Blockchain&lt;\/h2&gt;\n    &lt;div style=\"display:flex; gap:0.6em; margin-bottom:0.6em;\"&gt;\n      &lt;button class=\"btn\" id=\"btnRead\"&gt;Read Message&lt;\/button&gt;\n      &lt;button class=\"btn\" id=\"btnList\"&gt;List Files&lt;\/button&gt;\n    &lt;\/div&gt;\n    &lt;div class=\"message-display\" id=\"readResult\"&gt;No data yet \u2014 click \"Read Message\" after writing.&lt;\/div&gt;\n  &lt;\/div&gt;\n\n  &lt;!-- Log --&gt;\n  &lt;div class=\"section\" style=\"flex:1; display:flex; flex-direction:column;\"&gt;\n    &lt;h2&gt;\ud83d\udccb Event Log&lt;\/h2&gt;\n    &lt;div class=\"log-area\" id=\"logArea\"&gt;Hello Blockchain dApp initialized.&lt;\/div&gt;\n  &lt;\/div&gt;\n&lt;\/div&gt;\n`;\n\n\/\/ \u2500\u2500 dApp Class \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nclass CHelloBlockchain extends CWindow {\n\n  constructor(positionX, positionY, width, height, data, dataType, filePath, thread) {\n    super(positionX, positionY, width, height, helloBody,\n      \"Hello Blockchain\", CHelloBlockchain.getIcon(), true);\n\n    this.mTools = CTools.getInstance();\n    this.mMetaParser = new CVMMetaParser();\n    this.mPendingReadFile = false;\n\n    \/\/ Set a unique thread ID for this dApp instance\n    this.setThreadID = 'HELLO_BLOCKCHAIN_' + this.getProcessID;\n\n    \/\/ Register all event listeners\n    const ctx = CVMContext.getInstance();\n    ctx.addConnectionStatusChangedListener(this.onConnectionChanged.bind(this), this.mID);\n    ctx.addVMStateChangedListener(this.onVMStateChanged.bind(this), this.mID);\n    ctx.addNewDFSMsgListener(this.onDFSMessage.bind(this), this.mID);\n    ctx.addVMMetaDataListener(this.onVMMetaData.bind(this), this.mID);\n    ctx.addNewGridScriptResultListener(this.onGridScriptResult.bind(this), this.mID);\n    ctx.addVMCommitStateChangedListener(this.onCommitStateChanged.bind(this), this.mID);\n  }\n\n  \/\/ \u2500\u2500 Package Info \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n  static getPackageID() {\n    return \"org.gridnetproject.UIdApps.helloBlockchain\";\n  }\n  static getDefaultCategory() { return 'dApps'; }\n  static getIcon() {\n    \/\/ A simple chain-link icon (replace with your own Base64 PNG)\n    return '';\n  }\n\n  \/\/ \u2500\u2500 Lifecycle \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n  open() {\n    super.open();\n\n    \/\/ Bind button click events\n    this.getControl('btnWrite').addEventListener('click', () =&gt; this.writeMessage());\n    this.getControl('btnCommit').addEventListener('click', () =&gt; this.commitChanges());\n    this.getControl('btnRead').addEventListener('click', () =&gt; this.readMessage());\n    this.getControl('btnList').addEventListener('click', () =&gt; this.listFiles());\n\n    \/\/ Allow Enter key to trigger write\n    this.getControl('msgInput').addEventListener('keydown', (e) =&gt; {\n      if (e.key === 'Enter') this.writeMessage();\n    });\n\n    this.log('dApp started. Waiting for connection...');\n\n    \/\/ Check if already connected\n    if (CVMContext.getInstance().getConnectionState == eConnectionState.connected) {\n      this.onConnectionChanged({ state: eConnectionState.connected });\n    }\n  }\n\n  closeWindow() {\n    \/\/ Listeners registered with this.mID are auto-cleaned\n    super.closeWindow();\n  }\n\n  \/\/ \u2500\u2500 Core Actions \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n  writeMessage() {\n    const input = this.getControl('msgInput');\n    const message = input.value.trim();\n\n    if (!message) {\n      this.log('\u26a0\ufe0f Please enter a message first.');\n      return;\n    }\n\n    if (CVMContext.getInstance().getConnectionState != eConnectionState.connected) {\n      this.log('\u274c Not connected to GRIDNET Core.');\n      return;\n    }\n\n    const fs = CVMContext.getInstance().getFileSystem;\n\n    \/\/ Write the message as a file called \"hello_message.txt\"\n    let result = fs.doNewFile('hello_message.txt', message, false, this.getThreadID);\n    this.addNetworkRequestID(result.getReqID);\n\n    this.log('\u270d\ufe0f Writing \"' + message + '\" to hello_message.txt...');\n    this.log('\ud83d\udca1 Click \"Commit\" to persist this to the blockchain.');\n  }\n\n  commitChanges() {\n    if (CVMContext.getInstance().getConnectionState != eConnectionState.connected) {\n      this.log('\u274c Not connected to GRIDNET Core.');\n      return;\n    }\n\n    const fs = CVMContext.getInstance().getFileSystem;\n    let result = fs.doCommit();\n    this.addNetworkRequestID(result.getReqID);\n\n    this.log('\ud83d\udd10 Commit requested \u2014 awaiting authorization...');\n  }\n\n  readMessage() {\n    if (CVMContext.getInstance().getConnectionState != eConnectionState.connected) {\n      this.log('\u274c Not connected to GRIDNET Core.');\n      return;\n    }\n\n    const fs = CVMContext.getInstance().getFileSystem;\n    this.mPendingReadFile = true;\n\n    \/\/ First sync to get latest state, then read the file\n    fs.doSync();\n\n    let result = fs.doGetFile('hello_message.txt', false, this.getThreadID);\n    this.addNetworkRequestID(result.getReqID);\n\n    this.log('\ud83d\udcd6 Reading hello_message.txt from blockchain...');\n  }\n\n  listFiles() {\n    if (CVMContext.getInstance().getConnectionState != eConnectionState.connected) {\n      this.log('\u274c Not connected to GRIDNET Core.');\n      return;\n    }\n\n    const fs = CVMContext.getInstance().getFileSystem;\n\n    \/\/ Navigate to root and list\n    let result = fs.doCD('\/', true, false, false, this.getThreadID);\n    this.addNetworkRequestID(result.getReqID);\n\n    \/\/ Also request a standalone LS\n    let lsResult = fs.doLS(this.getThreadID);\n    this.addNetworkRequestID(lsResult.getReqID);\n\n    this.log('\ud83d\udcc2 Listing files...');\n  }\n\n  \/\/ \u2500\u2500 Event Callbacks \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n  onConnectionChanged(event) {\n    const dot = this.getControl('dotConn');\n    const lbl = this.getControl('lblConn');\n\n    if (event.state == eConnectionState.connected) {\n      dot.classList.add('ok');\n      lbl.textContent = 'Connected';\n      this.log('\u2705 Connected to GRIDNET Core!');\n    } else if (event.state == eConnectionState.connecting) {\n      lbl.textContent = 'Connecting...';\n    } else {\n      dot.classList.remove('ok');\n      lbl.textContent = 'Disconnected';\n      this.log('\u274c Disconnected.');\n    }\n  }\n\n  onVMStateChanged(event) {\n    const dot = this.getControl('dotVM');\n    const lbl = this.getControl('lblVM');\n\n    if (event.state == eVMState.ready || event.state == eVMState.synced) {\n      dot.classList.add('ok');\n      lbl.textContent = 'VM: Ready';\n      this.log('\u2705 Decentralized VM ready.');\n    } else if (event.state == eVMState.initializing) {\n      lbl.textContent = 'VM: Init...';\n    }\n  }\n\n  onCommitStateChanged(state) {\n    const dot = this.getControl('dotCommit');\n    const lbl = this.getControl('lblCommit');\n\n    switch (state) {\n      case eCommitState.prePending:\n        lbl.textContent = 'Commit: Preparing';\n        this.log('\ud83d\udd12 Commit preparing...');\n        break;\n      case eCommitState.pending:\n        dot.classList.add('ok');\n        lbl.textContent = 'Commit: Pending';\n        this.log('\u23f3 Commit pending \u2014 awaiting consensus...');\n        break;\n      case eCommitState.success:\n        dot.classList.add('ok');\n        lbl.textContent = 'Commit: \u2705 Success!';\n        this.log('\ud83c\udf89 COMMIT SUCCESSFUL! Your data is now permanently on the blockchain!');\n        \/\/ Reset after 3 seconds\n        setTimeout(() =&gt; {\n          dot.classList.remove('ok');\n          lbl.textContent = 'Commit: Idle';\n        }, 3000);\n        break;\n      case eCommitState.aborted:\n        dot.classList.remove('ok');\n        lbl.textContent = 'Commit: Aborted';\n        this.log('\u274c Commit aborted.');\n        break;\n      default:\n        dot.classList.remove('ok');\n        lbl.textContent = 'Commit: Idle';\n    }\n  }\n\n  onDFSMessage(dfsMsg) {\n    if (!this.hasNetworkRequestID(dfsMsg.getReqID)) return;\n\n    if (dfsMsg.getData1.byteLength &gt; 0) {\n      let metaData = this.mMetaParser.parse(dfsMsg.getData1);\n\n      if (metaData != 0) {\n        let sections = this.mMetaParser.getSections;\n\n        for (let i = 0; i &lt; sections.length; i++) {\n          let sType = sections[i].getType;\n\n          if (sType == eVMMetaSectionType.fileContents) {\n            let entries = sections[i].getEntries;\n\n            for (let a = 0; a &lt; entries.length; a++) {\n              let fields = entries[a].getFields;\n              let entryType = entries[a].getType;\n\n              if (entryType == eDFSElementType.fileContent) {\n                let fileName = this.mTools.arrayBufferToString(fields[1]);\n                let dataType = fields[0];\n                let rawData = fields[2];\n\n                let value;\n                if (dataType == eDataType.bytes) {\n                  value = this.mTools.arrayBufferToString(rawData);\n                } else if (dataType == eDataType.unsignedInteger ||\n                           dataType == eDataType.signedInteger) {\n                  value = this.mTools.arrayBufferToNumber(rawData);\n                } else {\n                  value = '[binary: ' + rawData.byteLength + ' bytes]';\n                }\n\n                if (fileName === 'hello_message.txt' || this.mPendingReadFile) {\n                  this.getControl('readResult').textContent = value;\n                  this.mPendingReadFile = false;\n                  this.log('\ud83d\udcc4 Read ' + fileName + ': \"' + value + '\"');\n                } else {\n                  this.log('\ud83d\udcc4 ' + fileName + ' = ' + value);\n                }\n              }\n              else if (entryType == eDFSElementType.directoryEntry) {\n                let dirName = this.mTools.arrayBufferToString(fields[1]);\n                this.log('\ud83d\udcc1 [DIR]  ' + dirName);\n              }\n              else if (entryType == eDFSElementType.fileEntry) {\n                let fileName = this.mTools.arrayBufferToString(fields[1]);\n                this.log('\ud83d\udcc4 [FILE] ' + fileName);\n              }\n              else if (entryType == eDFSElementType.stateDomainEntry) {\n                let domainName = this.mTools.arrayBufferToString(fields[1]);\n                this.log('\ud83c\udf10 [DOMAIN] ' + domainName);\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n\n  onVMMetaData(metaMsg) {\n    if (!this.hasNetworkRequestID(metaMsg.getReqID)) return;\n    this.log('\ud83d\udce1 Meta-data response received.');\n  }\n\n  onGridScriptResult(result) {\n    if (result == null) return;\n    this.log('\u26a1 GridScript execution completed.');\n  }\n\n  \/\/ \u2500\u2500 Utility \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n  log(message) {\n    const area = this.getControl('logArea');\n    if (!area) return;\n    const ts = new Date().toLocaleTimeString();\n    area.textContent += '\\n[' + ts + '] ' + message;\n    area.scrollTop = area.scrollHeight;\n  }\n}\n\n\/\/ Static settings field\nCHelloBlockchain.sCurrentSettings = null;\n\nexport default CHelloBlockchain;\n<\/pre>\n<h3>How to Use This dApp<\/h3>\n<ol>\n<li><strong>Save<\/strong> the code above as <code>HelloBlockchain.app<\/code><\/li>\n<li><strong>Open<\/strong> GRIDNET OS in your browser (<code>https:\/\/ui.gridnet.org<\/code> or your local instance)<\/li>\n<li><strong>Drag and drop<\/strong> the <code>.app<\/code> file onto the Desktop or File Manager<\/li>\n<li>The dApp window opens. Watch the status indicators turn green as it connects.<\/li>\n<li><strong>Type a message<\/strong> and click <strong>Write<\/strong> \u2014 this stages the file locally<\/li>\n<li>Click <strong>Commit<\/strong> \u2014 authorise the transaction when prompted<\/li>\n<li>After the commit succeeds, click <strong>Read Message<\/strong> \u2014 your message is read back from the blockchain!<\/li>\n<li>Click <strong>List Files<\/strong> to see all files in your current directory<\/li>\n<\/ol>\n<p>Congratulations. You have just built a decentralized application that reads and writes to a blockchain \u2014 using nothing but HTML, CSS, and JavaScript.<\/p>\n<h2>What Just Happened \u2014 The Full Picture<\/h2>\n<p>Let&#8217;s step back and see the complete journey your data takes:<\/p>\n<p><!-- Full lifecycle SVG --><\/p>\n<ol>\n<li><strong>Write<\/strong>: <code>doNewFile()<\/code> sends your data to GRIDNET Core via WebSocket. The data is staged locally in the node&#8217;s pending state.<\/li>\n<li><strong>Stage<\/strong>: Your changes exist in a local buffer. They&#8217;re real to your session but not yet committed to the blockchain.<\/li>\n<li><strong>Commit<\/strong>: <code>doCommit()<\/code> requests the node to package all staged changes into a blockchain transaction.<\/li>\n<li><strong>Sign<\/strong>: The transaction is cryptographically signed \u2014 either by scanning a QR code with the GRIDNET mobile wallet, or via a locally stored keychain.<\/li>\n<li><strong>Consensus<\/strong>: The signed transaction is broadcast to the peer-to-peer network. Other nodes validate it. Once consensus is reached, it&#8217;s included in the next data block.<\/li>\n<li><strong>On-chain<\/strong>: Your data is now permanently and immutably stored on the GRIDNET OS blockchain. Any node anywhere in the world can read it.<\/li>\n<\/ol>\n<p>The entire round-trip \u2014 from typing a message in your browser to having it permanently stored on a decentralized blockchain \u2014 takes seconds. And you wrote it in HTML, CSS, and JavaScript.<\/p>\n<h2>What to Build Next<\/h2>\n<p>You&#8217;ve built your first dApp. Here&#8217;s where to go from here:<\/p>\n<ul>\n<li><strong>Explore the existing dApps<\/strong>: Study the source code of Terminal.js, FileManager.js, and the Wallet dApp for production-grade patterns<\/li>\n<li><strong>Read the <a href=\"https:\/\/gridnet.org\/wpp\/index.php\/2026\/02\/18\/gridnet-os-ui-dapp-design-guidelines-the-complete-developer-reference\/\">UI dApp Design Guidelines<\/a><\/strong> \u2014 the complete reference for all rules, patterns, and best practices<\/li>\n<li><strong>Learn GridScript<\/strong> \u2014 the <a href=\"https:\/\/gridnet.org\/wpp\/index.php\/2026\/02\/18\/gridscript-the-language-of-the-decentralized-state-machine-a-comprehensive-guide\/\">GridScript Comprehensive Guide<\/a> covers the stack-based language in depth, including smart contracts and advanced operations<\/li>\n<li><strong>Use the Blockchain Explorer API<\/strong> \u2014 query blocks, transactions, and domain data programmatically through CVMContext<\/li>\n<li><strong>Implement State-Less Channels<\/strong> \u2014 for instant, off-chain micropayments between users<\/li>\n<li><strong>Build with WebRTC Swarms<\/strong> \u2014 for peer-to-peer real-time communication directly between browsers<\/li>\n<li><strong>Deploy Identity Tokens<\/strong> \u2014 create on-chain identities and token pools for your application&#8217;s economy<\/li>\n<\/ul>\n<p>The entire GRIDNET OS platform is open. There are no gatekeepers. No review boards. No permission required. You write code, you deploy it to the blockchain, and it runs \u2014 decentralized, censorship-resistant, and permanent. Welcome to the future of application development.<\/p>\n<p>Welcome to GRIDNET OS. \ud83c\udf10<\/p>\n","protected":false},"excerpt":{"rendered":"<p>You Already Know Everything You Need Here is the single most important sentence in this entire article: if you can write HTML,&#8230;<\/p>\n","protected":false},"author":1,"featured_media":835482,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[162,17,163],"tags":[213,155,164,197,145,142,143,159,152,212,165],"class_list":["post-835486","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-development","category-tutorial","category-tutorials","tag-beginner","tag-cvmcontext","tag-dapp","tag-dapps","tag-decentralized","tag-gridnet-os","tag-gridscript","tag-tutorial","tag-ui-dapp","tag-ui-development","tag-web-development"],"_links":{"self":[{"href":"https:\/\/mag.gridnet.org\/index.php\/wp-json\/wp\/v2\/posts\/835486","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/mag.gridnet.org\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/mag.gridnet.org\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/mag.gridnet.org\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/mag.gridnet.org\/index.php\/wp-json\/wp\/v2\/comments?post=835486"}],"version-history":[{"count":6,"href":"https:\/\/mag.gridnet.org\/index.php\/wp-json\/wp\/v2\/posts\/835486\/revisions"}],"predecessor-version":[{"id":835525,"href":"https:\/\/mag.gridnet.org\/index.php\/wp-json\/wp\/v2\/posts\/835486\/revisions\/835525"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/mag.gridnet.org\/index.php\/wp-json\/wp\/v2\/media\/835482"}],"wp:attachment":[{"href":"https:\/\/mag.gridnet.org\/index.php\/wp-json\/wp\/v2\/media?parent=835486"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/mag.gridnet.org\/index.php\/wp-json\/wp\/v2\/categories?post=835486"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/mag.gridnet.org\/index.php\/wp-json\/wp\/v2\/tags?post=835486"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}