I made a One-Page Antelope (EOS) Dapp

in #programming4 months 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.
Sort:  

This is awesome! It's fascinating to take a look into the technical, behind-the-scenes landscape that is powering Rstory. Can't wait to work on that video!

Thanks for all of your support. It means a lot: )

Writing this DAPP must have been a lot of work and I must say I big kudos to you for completing it and making it go live
Congratulations

Hey thanks! It was a lot of work but I love bringing good ideas to life.