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
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
}
}
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
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
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
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;
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
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.
Top comments (0)