I made a One-Page Antelope (EOS) Dapp

in programming •  16 days ago

    AEGHlMvYMfuwL2RZb7fx--1--qilwk.jpg

    The above image was made with stable diffusion using the prompt '"RSTORY" sign urban landscape colorful computer code rain.'

    For years I fantasized about creating a particular kind of Dapp. Recent developments in tech turned this fantasy into a realistic possibility. So I made the Dapp I envisioned. It went live today. Now, anyone who holds one or more Rstory tokens in their Anchor wallet can access my scifi ebooks for free, while those without Rstory cannot access the digital products.

    The Dapp is a client side one-pager, making it ideally suited to live on IPFS. It connects to Anchor wallet, confirms the account name, then connects to the EOS blockchain to check the account's Rstory balance. If the balance is positive, encrypted IPFS hashes are decrypted and used to construct active links for the digital products.

    This is not a secure method of hiding information. It would be trivial for a skilled adversary to decode the product urls. Under no circumstances should this or anything like it be used to hide sensitive data. In my case, a skilled adversary could break the code and gain access to my novels. Then what? They read them? Seems like a win to me.

    Writing this Dapp took me way out of my comfort zone. I started out with a Pyscript page but found myself having to move more and more of the logic from python, which I know, to javascript, which I can barely decipher. By the end, I could've cut Pyscript out entirely but I decided to leave it in place, ready for features I want to add in the future.

    The biggest challenge I encountered was correctly connecting to Anchor and EOS. After days of reading docs and Github I was stumped. The WharfKit SDK for EOS/Antelope looked perfect, but it was made for node and yarn, not client-side one-pagers. It seemed like there was a way to bundle all of the WharfKit files into a single javascript file, I just couldn't figure out how to do that.

    After several days on Telegram Antelope dev groups, someone named L. came through with a major assist. L. bundled the code I needed and sent me a link. Although this code was written for Wax, it wasn't to hard to adapt it to EOS. Chat-gpt helped rewrite it in vanilla javascript and integrating the logic went alright. Here are the critical scripts:

    <link rel="stylesheet" href="https://pyscript.net/releases/2024.5.2/core.css" />
    <script type="module" src="https://pyscript.net/releases/2024.5.2/core.js"></script>
    <script src="https://rstory.mypinata.cloud/ipfs/QmRRRaytjur3gspxa9mUxaxLQhW6A19qRvp4TFweJfACkn"></script>
    <script src="https://rstory.mypinata.cloud/ipfs/QmPrgG4X6sX3CHLLgrcQU8H8VTqh5mWqWfP6KLtfSfYKxc"></script>
    <script src="https://unpkg.com/anchor-link@3"></script>
    <script src="https://unpkg.com/anchor-link-browser-transport@3"></script>
    

    Here's all that's left of the python. Defining an element class and using it to display a welcome message.

    <py-config>
        packages = [
            "pyodide-http",
        ]
    </py-config>
      
    <script type="py">
    import re
    import js
    from js import console, document, fetch, window
    from pyscript import when, display
    import pyodide_http
    from pyodide.http import open_url
    from pyodide.ffi import create_proxy
    import asyncio
    
    pyodide_http.patch_all()
    
    # Re-implementing the Element class
    class Element:
        def __init__(self, element_id, element=None):
            self._id = element_id
            self._element = element
    
        @property
        def id(self):
            return self._id
    
        @property
        def element(self):
            """Return the dom element"""
            if not self._element:
                self._element = js.document.querySelector(f"#{self._id}")
            return self._element
    
        @property
        def value(self):
            return self.element.value
    
        @property
        def innerHtml(self):
            return self.element.innerHTML
    
        def write(self, value, append=False):
            if not append:
                self.element.innerHTML = value
            else:
                self.element.innerHTML += value
    
        def clear(self):
            if hasattr(self.element, "value"):
                self.element.value = ""
            else:
                self.write("", append=False)
    
        def select(self, query, from_content=False):
            el = self.element
    
            if from_content:
                el = el.content
    
            _el = el.querySelector(query)
            if _el:
                return Element(_el.id, _el)
            else:
                js.console.warn(f"WARNING: can't find element matching query {query}")
    
        def clone(self, new_id=None, to=None):
            if new_id is None:
                new_id = self.element.id
    
            clone = self.element.cloneNode(True)
            clone.id = new_id
    
            if to:
                to.element.appendChild(clone)
                # Inject it into the DOM
                to.element.after(clone)
            else:
                # Inject it into the DOM
                self.element.after(clone)
    
            return Element(clone.id, clone)
    
        def remove_class(self, classname):
            classList = self.element.classList
            if isinstance(classname, list):
                classList.remove(*classname)
            else:
                classList.remove(classname)
    
        def add_class(self, classname):
            classList = self.element.classList
            if isinstance(classname, list):
                self.element.classList.add(*classname)
            else:
                self.element.classList.add(classname)
    
    Element('loading').write("Welcome to Rstory")
    </script>
    

    The javascript is a little more involved, but still fairly simple. I omitted a logout function because it didn't seem important, but maybe there's a reason I should put one in.

    <script>
    function custom_decode(encoded_str) {
        return decodeURIComponent(encoded_str.match(/.{1,2}/g).map(function (v) {
            return '%' + ('0' + v).slice(-2);
        }).join(''));
    }
    
    function reveal() {
        const links = document.querySelectorAll('#book-list a');
        const base_url = "https://rstory.mypinata.cloud/ipfs/";
        links.forEach(link => {
            const encoded_hash = link.getAttribute("data-encoded-hash");
            if (encoded_hash) {
                const decoded_hash = custom_decode(encoded_hash);
                link.href = base_url + decoded_hash;
            }
        });
    }
    
    // JavaScript login and token check functions
    const dapp = "rstory";
    let login_use = "";
    let wallet_userAccount = "none";
    let wallet_session = null;
    
    const eos = new waxjs.WaxJS({
        rpcEndpoint: 'https://eos.greymass.com'
    });
    
    const transport = new AnchorLinkBrowserTransport();
    const anchorLink = new AnchorLink({
        transport,
        chains: [{
            chainId: 'aca376f206b8fc25a6ed44dbdc66547c36c6c33e3a119ffbeaef943642f0e906',
            nodeUrl: 'https://eos.greymass.com',
        }]
    });
    
    function loginEos(anchor) {
        login_use = anchor;
        if (anchor) {
            login_anchor();
        } else {
            login_eosjs().then(function (retorno) {
                wallet_userAccount = retorno;
                checkToken();
            });
        }
    }
    
    function login_anchor() {
        anchorLink.login(dapp).then((result) => {
            wallet_session = result.session;
            wallet_userAccount = wallet_session.auth.actor;
            checkToken();
        });
    }
    
    async function login_eosjs() {
        try {
            let userAccount = await eos.login();
            return userAccount;
        } catch (e) {
            document.getElementById("autologin").innerHTML = e.message;
        }
        return false;
    }
    
    function checkToken() {
        const eosApiUrl = "https://eos.eosphere.io/";
        const TokenContract = "rstorytokens";
        const TokenSymbol = "RSTORY";
        const jsonData = JSON.stringify({ "json": true, "code": TokenContract, "scope": wallet_userAccount, "table": "accounts", "limit": 10 });
    
        fetch(eosApiUrl + "v1/chain/get_table_rows", {
            method: "POST",
            headers: {
                "Content-Type": "application/json"
            },
            body: jsonData
        })
            .then(response => response.json())
            .then(data => {
                let TokenFound = false;
                data.rows.forEach(value => {
                    if (value.balance) {
                        TokenFound = true;
                        reveal();
                        document.getElementById("loading").innerHTML = "You have " + value.balance;
                    }
                });
                if (!TokenFound) {
                    document.getElementById("loading").innerHTML = "You must possess RSTORY to access the material";
                }
            })
            .catch(error => {
                console.error(error);
            });
    }
    
    document.getElementById('read-btn').addEventListener('click', () => loginEos(true));
    </script>
    

    Overall I'm very pleased with how this project came together. My Rstory token now has unambiguous utility and small but measurable value. Having thought about this for a long time, seeing it actually manifest is satisfying. Eventually I may even try to get an exchange listing.


    Read Free Mind Gazette on Substack

    Read my novels:

    See my NFTs:

    • Small Gods of Time Travel is a 41 piece Tezos NFT collection on Objkt that goes with my book by the same name.
    • History and the Machine is a 20 piece Tezos NFT collection on Objkt based on my series of oil paintings of interesting people from history.
    • Artifacts of Mind Control is a 15 piece Tezos NFT collection on Objkt based on declassified CIA documents from the MKULTRA program.
      Authors get paid when people like you upvote their post.
      If you enjoyed what you read here, create your account today and start earning FREE VOILK!