DEV Community

ogbotemi-2000
ogbotemi-2000

Posted on • Updated on

"blocktcha"; a micro-payment CAPTCHA alternative on Stellar testnet blockchain

This is a submission for the Build Better on Stellar: Smart Contract Challenge : Build a dApp

What I Built

A CAPTCHA alternative that accepts micro-payments in XLM to authenticate users while using common bot checks and a sandboxed environment

Demo

https://blocktcha.vercel.app/

Image description

My Code

https://github.com/ogbotemi-2000/blocktcha

let http   = require('http'),
    fs     = require('fs'),
    path   = require('path'),
    config = require('./config.json'),
    jobs   = {
      GET:function(req, res, parts, fxn) {
        /** middlewares that respond to GET requests are called here */
        fxn = fs.existsSync(fxn='.'+parts.url+'.js')&&require(fxn)
        if(parts.query) console.log("::PARTS::", parts), req.query = parts.params, fxn&&fxn(req, res);
        return !!fxn;
      },
      POST:function(req, res, parts, fxn) {
        fxn = fs.existsSync(fxn='.'+parts.url+'.js')&&require(fxn),
        req.on('data', function(data) {
          /**create req.body and res.json because the invoked module requires them to be defined */
          req.body = (parts = urlParts('?'+(data=data.toString()))).params,
          console.log('::POST::', parts)
          fxn&&fxn(req, res)
        })
        return !!fxn;
      }
    },
    mime   = { js: 'application/javascript', css: 'text/css', html:'text/html' },
    cache  = {}; /** to store the strings of data read from files */

http.createServer((req, res, url, parts, data, verb)=>{
  ({ url } = parts =  urlParts(req.url)),
  /** data expected to be sent to the client, this approach does away with res.write and res.send in the jobs */
  res.json=obj=>res.end(JSON.stringify(obj)), // for vercel functions
  data = jobs[verb=req.method](req, res, parts),

  url = url === '/' ? 'index.html' : url,
  /** the code below could be moved to a job but it is left here to prioritize it */
  data || new Promise((resolve, rej, cached)=>{
    if (data) { resolve(/*dynamic data, exit*/); return; }

    /*(cached=cache[req.url])?resolve(cached):*/fs.readFile(path.join('./', url), (err, buf)=>{
      if(err) rej(err);
      else resolve(cache[req.url]=buf)
    })
  }).then(cached=>{
    res.writeHead(200, {
      'Access-Control-Allow-Origin': '*',
      'Content-type': mime[url.split('.').pop()]||''
   }),
   /** return dynamic data or static file that was read */
    // console.log("::PROMISE", [url]),
    res.end(cached)
  }).catch((err, str)=>{
    console.log(str='::ERROR::ENOENT '+err, [url])
    res.end("::ENOENT::An error occured, you may create and use an error page in lieu of this string")
  })
}).listen(config.PORT, _=>{
  console.log(`Server listening on PORT ${config.PORT}`)
})

function urlParts(url, params, query, is_html) {
    params = {}, query='',
    (url = decodeURIComponent(url)).replace(/\?[^]*/, e=>((query=e.replace('?', '')).split('&').forEach(e=>params[(e=e.split('='))[0]]=e[1]), '')),
    query &&= '?'+query,
    is_html = !/\.[^]+$/.test(is_html = (url = url.replace(query, '')).split('/').pop())||/\.html$/.test(is_html);
    return {
        params, query, url, is_html
    }
}

Enter fullscreen mode Exit fullscreen mode

The code above is the hand-written pure Node.js http server I wrote to mimic Vercel serverless functionality while offering more resilency

Journey

Ideation

I read Greg Brockman's article on what to build on Stellar where he mentioned an oft-requested feature of a micro-payment backed application. Building it on a blockchain as accessible as Stellar made the project easier to implement

User and domain registration

Users are assigned a UUID on their first submission of a unique domain and offered a template that they can use to begin using the widget on their sites that are under their registered domains

Image description

Payments UI and UX

The actual widget to enable the verification is in an iframe which automatically connects to the Freighter wallet extension if it is available or proceeds as usual if otherwise

Image description

Waiting period

Users are prompted to send some crypto to the provided wallet address and await a usually rapid confirmation thanks to the integrity of the Stellar blockchain, testnet nonetheless

Image description

Smart contract design

Here is what I could come up with, the purpose of the contract is for archival storage of little info such as timestamps, hashes of transactions. A better database implementation may suffice later on but a trick of persisting data on Vercel serverless backends is adequate for now: https://dev.to/ogbotemi2000/persist-data-in-vercelnextjs-serverless-backends-1h70

lib.rs

#![no_std]

use soroban_sdk::{contracttype, contract, contractimpl, Env, Address, Vec, vec};
use crate::extend_ttl::{extend_high, extend};
use crate::store::{read_balance, write_balance, Store};

#[derive(Clone)]
#[contracttype]
pub struct AccountData {
    pub balance: i128,
    pub trxes: i128,
    pub t_stamps: Vec<u64>,
}

#[contract]
pub struct Contract;

#[contractimpl]
impl Contract {

    pub fn register(env: Env, addr: Address) -> (Address, Option<AccountData>) {
        let account : Option<AccountData> = env.storage().persistent().get(&addr);
        match &account {
          Some(_ac) =>{

          }
          None =>{
            env.storage().persistent().set(&addr, &AccountData {
              balance: 0,
              trxes: 0,
              t_stamps: vec![&env, env.ledger().timestamp()],
            });
          }
        };
        (addr, account)
    }
    pub fn receive(env: Env, addr: Address, amount: i128) { 
        if amount < 0 {
            panic!("Negative amounts are disallowed {}", amount)
        }
        addr.require_auth();
        extend(&env);
        read_balance(&env, &addr, &amount, |env: &Env, addr: &Address, balance: &i128| {
            write_balance(env, addr, balance);
            extend_high(env, addr)
        });
    }

    pub fn store(env: Env, addr: Address) -> bool {
        Store::has(&env, &addr)
    }
}

mod test;

mod extend_ttl;

mod store;

Enter fullscreen mode Exit fullscreen mode

The application utilizes EventSource to listen to transaction events and validate users as senders thereof.

Soroban RPC


// ../node_modules/
import { Contract, SorobanRpc, TransactionBuilder, Networks, BASE_FEE, Keypair, nativeToScVal } from '../node_modules/@stellar/stellar-sdk';
import config from '../config.json';// assert {type: "json"};

let kP     = Keypair.fromSecret(config.SECRET_KEY),
    rpcUrl = 'https://soroban-testnet.stellar.org',
    caller = kP.publicKey(), 
    fxn    = 'register',
    params = {
        fee: BASE_FEE,
        networkPassphrase: Networks.TESTNET
    };

const provider      = new SorobanRpc.Server(rpcUrl, { allowHttp: true }),
      sourceAccount = await provider.getAccount(caller);

async function invoke(publicKey) {
    /**build transaction */
    let contract  = new Contract(config.CONTRACT_ID),
        buildTx   = new TransactionBuilder(sourceAccount, params)
            .addOperation(contract.call(fxn, ...toType(publicKey||config.PUBLIC_KEY, 'address')))
            .setTimeout(30)
            .build(),
        prepareTx = await provider.prepareTransaction(buildTx);

    // console.log('::XDR::', prepareTx.toXDR()),
    prepareTx.sign(kP);
    try {
      let sendTx = await provider.sendTransaction(prepareTx).catch(err=>err)
      if(sendTx.errorResult) throw new Error('Unable to send Tx');
      if (sendTx.status === "PENDING") {
        let txResponse = await provider.getTransaction(sendTx.hash);
        while(txResponse.status==="NOT_FOUND") {
            txResponse = await provider.getTransaction(sendTx.hash);
            await new Promise(resolve=>setTimeout(resolve, 100));
        }
        if(txResponse.status==="SUCCESS") {
            let res = txResponse.returnValue;
            console.log('::RESPONSE::', res._value[0])
        }
      }
    } catch (error) {

    }
}

console.log(toType(config.PUBLIC_KEY, 'address'), Networks.TESTNET);

function toType(value, type) {
    return [nativeToScVal(value, { type })]
}

export default invoke


Enter fullscreen mode Exit fullscreen mode

The Stellar blockchain is mature enough for real-world apps to be built on top of it. It is only left to rethink just what a blockchain really offers and how existing solutions can be improved via it.

With a low barrier of entry - readiness of testnet Lumens, it is clear that Stellar has long since been more than ripe to ride the wave of smart contracts that have intrinsic value and not hype.

Additional Prize Categories: Glorious Game and/or Super Sustainable

Top comments (0)