11 Commits

Author SHA1 Message Date
Peter Harpending 46345da283 we now spy keyblocks for spends, need to test but probably close to working 2026-03-02 18:54:33 -08:00
Peter Harpending bc870e5f2d wip 2026-03-02 18:27:05 -08:00
Peter Harpending 4ee1825b43 grids random payload defined
ok so next step is go on the server side
- extract recipient, amount, payload,
- register search pattern
- wait for matching transaction to appear
- notify correct party
2026-02-27 11:43:33 -08:00
Peter Harpending be6aceb41a start spy process 2026-02-27 11:34:33 -08:00
pharpend 8a9f060b2d made a little progress on grids demo
i mostly have my head wrapped around the problem a lot more, and need to just
let my brain sit
2026-02-25 18:22:26 -08:00
pharpend 9adbf67ebd shed biketh 2025-12-30 11:45:25 -08:00
pharpend ee35e6cf1f HTML BIKESHED 2025-12-29 15:41:29 -08:00
pharpend 3343dcf137 basic grids demo seems to work... 2025-12-29 14:04:03 -08:00
pharpend f0d1097f1f qr demo 2025-12-29 08:49:42 -08:00
pharpend 60803b4a4e start grids qr demo 2025-12-19 17:47:31 -08:00
pharpend 139f9cb9e4 add qr as a dep 2025-12-17 17:34:08 -08:00
19 changed files with 1532 additions and 7 deletions
+6
View File
@@ -1,3 +1,9 @@
2026-02-25(PRH):
grids: instead of spying the chain for a tx, let's use dead drop
- so for "generate", need to form tx for the user to sign
- make dead drop -> have process that knows each tx and the dead drop location
OPEN LOOPS - 2025-11-12 OPEN LOOPS - 2025-11-12
- websockets - websockets
- separate websocket handling from websocket parsing/sending - separate websocket handling from websocket parsing/sending
+37 -3
View File
@@ -1,9 +1,43 @@
# fewd = front end web dev fewd = front end web dev
=====================================================================
this is me (PRH) trying to learn some front end web dev because pixels are this is me (PRH) trying to learn some front end web dev because pixels are
important despite my wishes. important despite my wishes.
# notes Building/Running
---------------------------------------------------------------------
## goal queue ### Prereqs
1. [Install Erlang and ZX](https://git.qpq.swiss/QPQ-AG/research-megadoc/wiki/Installing-Erlang-and-zx)
2. **DEV ONLY**: `apt install node-typescript` (Devuan Excalibur)
This is needed if you want to **edit** the `.ts` files found in
`/priv/static/js/ts/*.ts`. The built JS files are under version control and can
be found in `/priv/static/js/dist/`
### Building/Running HTTP Server
If you are only changing the Erlang or simply just want to run the program
without developing it, then just run
```
zxh runlocal
```
### Building TS->JS
**This is only necessary if you edited the `.ts` files and want to transpile
them over to JS.**
This requires you installed `tsc` as above.
```
make tsc
```
If you're doing development you may want
```
make watch
```
View File
+20
View File
@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>FIXME</title>
<link rel="stylesheet" href="/css/default.css">
</head>
<body>
<div id="titlebar">
<div class="content">
<a href="/" class="tb-home">Home</a>
</div>
</div>
<div class="content">
<h1 class="content-title">FEWD: FIXME</h1>
</div>
</body>
</html>
+9
View File
@@ -0,0 +1,9 @@
/**
* Title: Title
* Description: Description
* Author: Peter Harpending <peterharpending@qpq.swiss>
* Date: YYYY-MM-DD
* Last-Updated: YYYY-MM-DD
*
* @module
*/
+60
View File
@@ -0,0 +1,60 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Basic GRIDS Demo</title>
<link rel="stylesheet" href="/css/default.css">
</head>
<body>
<div id="titlebar">
<div class="content">
<a href="/" class="tb-home">Home</a>
</div>
</div>
<div class="content">
<h1 class="content-title">FEWD: GRIDS DEMO</h1>
<h2>Making a Spend</h2>
<label for="grids-n">Network ID:</label>
<input type = "text"
id = "grids-n"
value = "groot.testnet"
disabled>
<br>
<label for="grids-r">Recipient:</label>
<input type = "text"
id = "grids-r"
value = "ak_n6aVQ6PkBdVdv7kRRcfnzDVmBsH6hqEwVWSB6UAEb3kkjrPMe"
disabled>
<br>
<label for="grids-a">Amount (P):</label>
<input type = "number"
id = "grids-a"
value = "69"
disabled>
<br>
<label for="grids-p">Payload:</label>
<input type = "text"
id = "grids-p"
disabled>
<br>
<input type = "button"
id = "grids-submit"
value = "Generate">
<br>
<textarea disabled id="grids-url" hidden></textarea>
<br>
<img id="grids-png" hidden>
</div>
<script src="/js/dist/grids-basic.js"></script>
</body>
</html>
+1
View File
@@ -17,6 +17,7 @@
<ul> <ul>
<li><a href="/echo.html">Echo</a></li> <li><a href="/echo.html">Echo</a></li>
<li><a href="/grids-basic.html">GRIDS: Basic Demo</a></li>
<li><a href="/wfc.html">WFC</a></li> <li><a href="/wfc.html">WFC</a></li>
</ul> </ul>
</div> </div>
+29
View File
@@ -0,0 +1,29 @@
/**
* Title: GRIDS Basic Page Script
* Description: Page Script for /grids-basic.html
* Author: Peter Harpending <peterharpending@qpq.swiss>
* Date: 2025-12-29
* Last-Updated: 2025-12-29
*
* @module
*/
/**
* Runs on page load
*/
declare function main(): Promise<void>;
declare function on_submit(n_input: HTMLInputElement, r_input: HTMLInputElement, a_input: HTMLInputElement, p_input: HTMLInputElement, grids_url_elt: HTMLTextAreaElement, grids_png_elt: HTMLImageElement): Promise<void>;
type Safe<t> = {
ok: true;
result: t;
} | {
ok: false;
error: string;
};
type GridsResult = {
url: string;
png_base64: string;
};
/**
* gets the grids url
*/
declare function grids_request(net_id: string, recipient: string, amount: number, payload: string): Promise<Safe<GridsResult>>;
+87
View File
@@ -0,0 +1,87 @@
"use strict";
/**
* Title: GRIDS Basic Page Script
* Description: Page Script for /grids-basic.html
* Author: Peter Harpending <peterharpending@qpq.swiss>
* Date: 2025-12-29
* Last-Updated: 2025-12-29
*
* @module
*/
main();
/**
* Runs on page load
*/
async function main() {
let n_input = document.getElementById('grids-n');
let r_input = document.getElementById('grids-r');
let a_input = document.getElementById('grids-a');
let p_input = document.getElementById('grids-p');
let submit_btn = document.getElementById('grids-submit');
let grids_url_elt = document.getElementById('grids-url');
let grids_png_elt = document.getElementById('grids-png');
let rand_data = new Uint8Array(32);
window.crypto.getRandomValues(rand_data);
let hex = rand_data.toHex().toUpperCase();
p_input.value = 'text payload ' + hex;
// Page initialization
submit_btn.addEventListener('click', async function (e) {
await on_submit(n_input, r_input, a_input, p_input, grids_url_elt, grids_png_elt);
});
// enable buttons
submit_btn.disabled = false;
}
async function on_submit(n_input, r_input, a_input, p_input, grids_url_elt, grids_png_elt) {
// pull out values
let network_id = n_input.value;
let recipient = r_input.value;
let amount = parseInt(a_input.value);
let payload = p_input.value;
let result = await grids_request(network_id, recipient, amount, payload);
// show url field and png
if (result.ok) {
let url = result.result.url;
let png_base64 = result.result.png_base64;
let src_prefix = 'data:image/png;base64,';
let src = src_prefix + png_base64;
grids_url_elt.innerText = url;
grids_png_elt.src = src;
grids_url_elt.hidden = false;
grids_png_elt.hidden = false;
}
else {
alert('ERROR: ' + result.error);
}
}
/**
* gets the grids url
*/
async function grids_request(net_id, recipient, amount, payload) {
// format for network transmission
let obj = { 'network_id': net_id,
'recipient': recipient,
'amount': amount,
'payload': payload };
let obj_text = JSON.stringify(obj, undefined, 4);
let url = '/grids-mkdd';
let req_options = { method: 'POST',
headers: { 'content-type': 'application/json' },
body: obj_text };
let result = { ok: false,
error: 'IT DO BE LIKE THAT MISTA STANCIL' };
try {
let response = await fetch(url, req_options);
if (response.ok)
result = await response.json();
else {
console.log('bad http response:', response);
result = { ok: false, error: 'BAD HTTP RESPONSE' };
}
}
catch (x) {
console.log('network error:', x);
result = { ok: false, error: 'NETWORK ERROR' };
}
return result;
}
//# sourceMappingURL=grids-basic.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"grids-basic.js","sourceRoot":"","sources":["../ts/grids-basic.ts"],"names":[],"mappings":"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"}
+136
View File
@@ -0,0 +1,136 @@
/**
* Title: GRIDS Basic Page Script
* Description: Page Script for /grids-basic.html
* Author: Peter Harpending <peterharpending@qpq.swiss>
* Date: 2025-12-29
* Last-Updated: 2025-12-29
*
* @module
*/
main();
/**
* Runs on page load
*/
async function
main
()
{
let n_input = document.getElementById('grids-n') as HTMLInputElement;
let r_input = document.getElementById('grids-r') as HTMLInputElement;
let a_input = document.getElementById('grids-a') as HTMLInputElement;
let p_input = document.getElementById('grids-p') as HTMLInputElement;
let submit_btn = document.getElementById('grids-submit') as HTMLInputElement;
let grids_url_elt = document.getElementById('grids-url') as HTMLTextAreaElement;
let grids_png_elt = document.getElementById('grids-png') as HTMLImageElement;
let rand_data : Uint8Array = new Uint8Array(32);
window.crypto.getRandomValues(rand_data);
let hex : string = rand_data.toHex().toUpperCase();
p_input.value = 'text payload ' + hex;
// Page initialization
submit_btn.addEventListener(
'click',
async function(e) {
await on_submit(n_input, r_input, a_input, p_input, grids_url_elt, grids_png_elt)
}
);
// enable buttons
submit_btn.disabled = false;
}
async function
on_submit
(n_input : HTMLInputElement,
r_input : HTMLInputElement,
a_input : HTMLInputElement,
p_input : HTMLInputElement,
grids_url_elt : HTMLTextAreaElement,
grids_png_elt : HTMLImageElement)
: Promise<void>
{
// pull out values
let network_id : string = n_input.value;
let recipient : string = r_input.value;
let amount : number = parseInt(a_input.value);
let payload : string = p_input.value;
let result: Safe<GridsResult> = await grids_request(network_id, recipient, amount, payload);
// show url field and png
if (result.ok) {
let url : string = result.result.url;
let png_base64 : string = result.result.png_base64;
let src_prefix : string = 'data:image/png;base64,'
let src = src_prefix + png_base64;
grids_url_elt.innerText = url;
grids_png_elt.src = src;
grids_url_elt.hidden = false;
grids_png_elt.hidden = false;
}
else {
alert('ERROR: ' + result.error);
}
}
type Safe<t> = {ok: true, result: t}
| {ok: false, error: string};
type GridsResult = {url : string,
png_base64: string};
/**
* gets the grids url
*/
async function
grids_request
(net_id : string,
recipient : string,
amount : number,
payload : string)
: Promise<Safe<GridsResult>>
{
// format for network transmission
let obj : object = {'network_id' : net_id,
'recipient' : recipient,
'amount' : amount,
'payload' : payload};
let obj_text : string = JSON.stringify(obj, undefined, 4);
let url = '/grids-mkdd';
let req_options = {method: 'POST',
headers: {'content-type': 'application/json'},
body: obj_text};
let result: Safe<GridsResult> =
{ok : false,
error : 'IT DO BE LIKE THAT MISTA STANCIL'};
try {
let response : Response = await fetch(url, req_options);
if (response.ok)
result = await response.json() as Safe<GridsResult>;
else {
console.log('bad http response:', response);
result = {ok: false, error: 'BAD HTTP RESPONSE'};
}
}
catch (x: any) {
console.log('network error:', x);
result = {ok: false, error: 'NETWORK ERROR'};
}
return result;
}
+150
View File
@@ -0,0 +1,150 @@
% @doc grids cache
-module(fd_gridsd).
-vsn("0.2.0").
-behavior(gen_server).
-export_type([
]).
-export([
%% caller context
mkdd/4,
%% api
start_link/0,
%% process context
init/1, handle_call/3, handle_cast/2, handle_info/2,
code_change/3, terminate/2
]).
-include("$zx_include/zx_logger.hrl").
-type hex() :: binary().
-define(SEC, 1).
-define(MIN, 60*SEC).
-define(DEAD_DROP_TTL, 30*MIN).
-record(dd,
{created_at :: integer(),
payload :: string()}).
-type dd() :: #dd{}.
-record(s,
{drops = #{} :: #{hex() := dd()}}).
-type state() :: #s{}.
%%-----------------------------------------------------------------------------
%% caller context
%%-----------------------------------------------------------------------------
-spec mkdd(NetworkId, Recipient, Amount, Payload) -> Result
when NetworkId :: string(),
Recipient :: string(),
Amount :: non_neg_integer(),
Payload :: binary(),
Result :: {ok, URL, QR_PNG}
| {error, string()},
URL :: string(),
QR_PNG :: binary().
% @doc
% make a dead drop
mkdd(NetworkId, Recipient, Amount, Payload) ->
gen_server:call(?MODULE, {mkdd, NetworkId, Recipient, Amount, Payload}).
%% gen_server callbacks
start_link() ->
gen_server:start_link({local, ?MODULE}, ?MODULE, none, []).
%%-----------------------------------------------------------------------------
%% process context below this line
%%-----------------------------------------------------------------------------
%% gen_server callbacks
init(none) ->
tell("starting fd_gridsd"),
InitState = #s{},
{ok, InitState}.
handle_call({mkdd, NetworkId, Recipient, Amount, Payload}, From, State) ->
case do_mkdd(NetworkId, Recipient, Amount, Payload, State) of
{ok, URL, PNG, NewState} -> {reply, {ok, URL, PNG}, NewState};
Error -> {reply, Error, State}
end;
handle_call(Unexpected, From, State) ->
tell("~tp: unexpected call from ~tp: ~tp", [?MODULE, Unexpected, From]),
{noreply, State}.
handle_cast(Unexpected, State) ->
tell("~tp: unexpected cast: ~tp", [?MODULE, Unexpected]),
{noreply, State}.
handle_info(Unexpected, State) ->
tell("~tp: unexpected info: ~tp", [?MODULE, Unexpected]),
{noreply, State}.
code_change(_, State, _) ->
{ok, State}.
terminate(_, _) ->
ok.
%%-----------------------------------------------------------------------------
%% internals
%%-----------------------------------------------------------------------------
-spec do_mkdd(NetworkId, Recipient, Amount, Payload, State) -> Result
when NetworkId :: string(),
Recipient :: string(),
Amount :: pos_integer(),
Payload :: binary(),
State :: state(),
Result :: {ok, URL, QR_PNG, NewState}
| {error, Reason},
URL :: string(),
QR_PNG :: binary(),
NewState :: state(),
Reason :: any().
do_mkdd(NetId, Recipient, Amount, Payload, State = #s{drops = Drops}) ->
HexStr = rand_hex(),
CreatedAt = now_sec(),
TxStr = form_txstr(NetId, Recipient, Amount, Payload),
DD = #dd{created_at = CreatedAt, payload = TxStr},
URL = dd_url(HexStr),
PNG = qr:encode_png(unicode:characters_to_binary(URL)),
NewDrops = maps:put(HexStr, DD, Drops),
NewState = State#s{drops = NewDrops},
{ok, URL, PNG, NewState}.
now_sec() ->
calendar:datetime_to_gregorian_seconds(calendar:universal_time()).
-spec rand_hex() -> hex().
rand_hex() ->
unicode:characters_to_binary(hexify(rand:bytes(10))).
hexify(<<B:8, Rest/binary>>) when B < 16#10 -> ["0", integer_to_list(B, 16), hexify(Rest)];
hexify(<<B:8, Rest/binary>>) -> [integer_to_list(B, 16), hexify(Rest)];
hexify(<<>>) -> [].
dd_url(HexStr) ->
unicode:characters_to_list(["grids://", fewd:host(), "/1/d/", HexStr]).
% ref
form_txstr(_NetId, _Recipient, _Amount, _Payload) ->
"foobar".
+33 -2
View File
@@ -238,8 +238,9 @@ route(Sock, get, Route, Request, Received) ->
end; end;
route(Sock, post, Route, Request, Received) -> route(Sock, post, Route, Request, Received) ->
case Route of case Route of
<<"/wfcin">> -> wfcin(Sock, Request) , Received; <<"/grids-mkdd">> -> grids_mkdd(Sock, Request) , Received;
_ -> fd_httpd_utils:http_err(Sock, 404) , Received <<"/wfcin">> -> wfcin(Sock, Request) , Received;
_ -> fd_httpd_utils:http_err(Sock, 404) , Received
end; end;
route(Sock, _, _, _, Received) -> route(Sock, _, _, _, Received) ->
fd_httpd_utils:http_err(Sock, 404), fd_httpd_utils:http_err(Sock, 404),
@@ -319,6 +320,36 @@ ws_echo_loop(Sock, Frames, Received) ->
error(Error) error(Error)
end. end.
%% ------------------------------
%% grids
%% ------------------------------
grids_mkdd(Sock, #request{enctype = json,
body = B = #{"network_id" := NetId,
"recipient" := Recipient,
"amount" := Amount,
"payload" := Payload}}) ->
tell("grids_mkdd good request: ~tp", [B]),
RespObj =
case fd_gridsd:mkdd(NetId, Recipient, Amount, unicode:characters_to_binary(Payload)) of
{ok, URL, PNG} ->
#{"ok" => true,
"result" => #{"url" => URL,
"png_base64" => unicode:characters_to_list(base64:encode(PNG))}};
{error, String} ->
#{"ok" => false,
"error" => String}
end,
Body = zj:encode(RespObj),
% update cache with new context
Response = #response{headers = [{"content-type", "application/json"}],
body = Body},
fd_httpd_utils:respond(Sock, Response);
grids_mkdd(Sock, Request) ->
tell("grids_mkdd: bad request: ~tp", [Request]),
fd_httpd_utils:http_err(Sock, 400).
%% ------------------------------ %% ------------------------------
%% wfc %% wfc
+84
View File
@@ -0,0 +1,84 @@
% @doc hz helper functions
-module(fd_hz).
-export_type([
]).
-export([
txs_current/0,
txs_since_height/1,
txs_minus/1,
txs_from_to/2,
current_height/0,
txs_of_height/1,
mhs_of_height/1,
txs_of_mh/1,
filter_spends/1,
filter_spend/1,
spend_info/1,
test/0
]).
-spec test() -> no_return().
test() ->
io:format("~tp~n", [hz:gen_current()]),
io:format("~tp~n", [hz:kb_current()]),
ok.
txs_current() ->
H = current_height(),
txs_of_height(H).
txs_since_height(H) ->
txs_from_to(H, current_height()).
txs_minus(N) ->
H = current_height(),
L = H - N,
txs_from_to(L, H).
txs_from_to(Min, Max) when Min =< Max ->
lists:append([txs_of_height(H) || H <- lists:seq(Min, Max)]);
txs_from_to(Min, Max) when Min > Max ->
[].
current_height() ->
{ok, H} = hz:kb_current_height(),
H.
txs_of_height(Height) ->
lists:append([txs_of_mh(MH) || MH <- mhs_of_height(Height)]).
-spec mhs_of_height(Height :: pos_integer()) -> term().
mhs_of_height(Height) ->
{ok, #{"micro_blocks" := MHs}} = hz:gen_by_height(Height),
MHs.
txs_of_mh(MH) ->
{ok, TXs} = hz:mb_txs(MH),
TXs.
filter_spends(TXs) ->
lists:filtermap(fun filter_spend/1, TXs).
filter_spend(#{"tx" := TX = #{"type" := "SpendTx"}}) -> {true, spend_info(TX)};
filter_spend(_) -> false.
spend_info(#{"type" := "SpendTx",
"recipient_id" := Recipient,
"amount" := Amount,
"payload" := Payload}) ->
{sp, Recipient, Amount, Payload}.
+213
View File
@@ -0,0 +1,213 @@
% @doc spy: knows constraints {recipient, amount, payload}
%
% spies the chain for transactions that satisfy that constraint
%
-module(fd_spy).
-vsn("0.2.0").
% MVP: register search patterns
% simply print to console when one of them is found
-behavior(gen_server).
-export_type([
]).
-export([
%% caller context
reg/3,
%% api
start_link/0,
%% process context
init/1, handle_call/3, handle_cast/2, handle_info/2,
code_change/3, terminate/2
]).
% recipient is a string
-type pubkey32() :: <<_:256>>.
-record(sp,
{recipient :: pubkey32(),
amount :: pos_integer(),
payload :: binary()}).
-type search_pattern() :: #sp{}.
-record(s,
{last_height_seen = none :: none | integer(),
searching_for = [] :: [search_pattern()]}).
-type state() :: #s{}.
-include("$zx_include/zx_logger.hrl").
%%-----------------------------------------------------------------------------
%% caller context
%%-----------------------------------------------------------------------------
-spec reg(Recipient, Amount, Payload) -> ok | {error, Reason}
when Recipient :: pubkey32(),
Amount :: pos_integer(),
Payload :: binary(),
Reason :: any().
reg(R, A, P) when is_binary(R), byte_size(R) =:= 32,
is_integer(A), A >= 0,
is_binary(P) ->
gen_server:call(?MODULE, {reg, R, A, P}).
%% gen_server callbacks
start_link() ->
gen_server:start_link({local, ?MODULE}, ?MODULE, none, []).
%%-----------------------------------------------------------------------------
%% process context below this line
%%-----------------------------------------------------------------------------
%% gen_server callbacks
-spec init(none) -> {ok, state()}.
init(none) ->
tell("starting fd_spy"),
hz:chain_nodes(fewd:chain_nodes()),
erlang:send_after(1000, self(), check_chain),
InitState = #s{},
{ok, InitState}.
-spec handle_call(Msg, From, State) -> Result
when Msg :: any(),
From :: {pid(), reference()},
State :: state(),
Result :: {reply, Reply, NewState}
| {noreply, NewState},
Reply :: any(),
NewState :: state().
handle_call({reg, Recipient, Amount, Payload}, _From, State) ->
{Reply, NewState} = do_reg(Recipient, Amount, Payload, State),
{reply, Reply, NewState};
handle_call(Unexpected, From, State) ->
tell("~tp: unexpected call from ~tp: ~tp", [?MODULE, Unexpected, From]),
{noreply, State}.
handle_cast(Unexpected, State) ->
tell("~tp: unexpected cast: ~tp", [?MODULE, Unexpected]),
{noreply, State}.
handle_info(check_chain, State) ->
NewState = do_check_chain(State),
erlang:send_after(1000, self(), check_chain),
{noreply, NewState};
handle_info(Unexpected, State) ->
tell("~tp: unexpected info: ~tp", [?MODULE, Unexpected]),
{noreply, State}.
code_change(_, State, _) ->
{ok, State}.
terminate(_, _) ->
ok.
%%-----------------------------------------------------------------------------
%% internals
%%-----------------------------------------------------------------------------
do_check_chain(State = #s{last_height_seen = none}) ->
case hz:kb_current_height() of
{ok, Max} ->
hh(Max-1, Max, State);
Error ->
tell("~tp hz error: ~tp", [?MODULE, Error]),
State
end;
do_check_chain(State = #s{last_height_seen = Min}) ->
case hz:kb_current_height() of
{ok, Max} ->
hh(Min, Max, State);
Error ->
tell("~tp hz error: ~tp", [?MODULE, Error]),
State
end.
% handle height
hh(PrevHeight, NewHeight, State) when PrevHeight < NewHeight ->
tell("~tp cool: PrevHeight=~tp, NewHeight=~tp", [?MODULE, PrevHeight, NewHeight]),
Spends = fd_hz:filter_spends(fd_hz:txs_from_to(PrevHeight + 1, NewHeight)),
tell("~tp spends: ~tp", [?MODULE, Spends]),
NewState = State#s{last_height_seen = NewHeight},
NewState;
hh(PrevHeight, NewHeight, State) when PrevHeight >= NewHeight ->
log(info, "~tp lame: PrevHeight=~tp, NewHeight=~tp", [?MODULE, PrevHeight, NewHeight]),
State.
-spec do_reg(Recipient, Amount, Payload, State) -> {Reply, NewState}
when Recipient :: pubkey32(),
Amount :: pos_integer(),
Payload :: binary(),
State :: state(),
Reply :: ok
| {error, Reason},
Reason :: any(),
NewState :: state().
do_reg(Recipient, Amount, Payload, State) ->
case already_registered(Recipient, Payload, State) of
true -> {error, already_registered};
false -> really_register(Recipient, Amount, Payload, State)
end.
-spec already_registered(Recipient, Payload, State) -> boolean()
when Recipient :: pubkey32(),
Payload :: binary(),
State :: state().
already_registered(Recipient, Payload, _State = #s{searching_for = SPs}) ->
ar(Recipient, Payload, SPs).
ar(Recipient, Payload, [#sp{recipient=Recipient, payload=Payload} | _]) ->
true;
ar(Recipient, Payload, [_ | Rest]) ->
ar(Recipient, Payload, Rest);
ar(_, _, []) ->
false.
-spec really_register(Recipient, Amount, Payload, State) -> {Reply, NewState}
when Recipient :: pubkey32(),
Amount :: pos_integer(),
Payload :: binary(),
State :: state(),
Reply :: ok
| {error, Reason},
Reason :: any(),
NewState :: state().
really_register(R, A, P, State = #s{searching_for = SPs}) ->
NewSearchPattern = #sp{recipient = R,
amount = A,
payload = P},
tell("~tp: really_register(~tp,~tp,~tp)", [?MODULE, R, A, P]),
NewSearchPatterns = [NewSearchPattern | SPs],
NewState = State#s{searching_for = NewSearchPatterns},
{reply, ok, NewState}.
+13 -1
View File
@@ -36,6 +36,18 @@ start_link() ->
init([]) -> init([]) ->
RestartStrategy = {one_for_one, 1, 60}, RestartStrategy = {one_for_one, 1, 60},
Spy = {fd_spy,
{fd_spy, start_link, []},
permanent,
5000,
worker,
[fd_spy]},
GridsD = {fd_gridsd,
{fd_gridsd, start_link, []},
permanent,
5000,
worker,
[fd_gridsd]},
WFCd = {fd_wfcd, WFCd = {fd_wfcd,
{fd_wfcd, start_link, []}, {fd_wfcd, start_link, []},
permanent, permanent,
@@ -48,5 +60,5 @@ init([]) ->
5000, 5000,
supervisor, supervisor,
[fd_httpd]}, [fd_httpd]},
Children = [WFCd, Httpd], Children = [Spy, GridsD, WFCd, Httpd],
{ok, {RestartStrategy, Children}}. {ok, {RestartStrategy, Children}}.
+23
View File
@@ -9,12 +9,34 @@
-copyright("Peter Harpending <peterharpending@qpq.swiss>"). -copyright("Peter Harpending <peterharpending@qpq.swiss>").
-license("BSD-2-Clause-FreeBSD"). -license("BSD-2-Clause-FreeBSD").
-export([chain_nodes/0, url/0, host/0, network_id/0, pubkey/0, akstr/0]).
-export([listen/1, ignore/0]). -export([listen/1, ignore/0]).
-export([start/2, stop/1]). -export([start/2, stop/1]).
-include("$zx_include/zx_logger.hrl"). -include("$zx_include/zx_logger.hrl").
%% for testing: use mainnet
%chain_nodes() ->
% [{"tsuriai.jp", 3013}].
chain_nodes() ->
[{"tsuriai.jp", 4013}].
url() -> "http://" ++ host().
host() -> "localhost:8000".
network_id() -> "groot.testnet".
pubkey() -> pad32(<<"fewd demo">>).
akstr() -> gmgrids:akstr(pubkey()).
pad32(Bytes) ->
BS = byte_size(Bytes),
Spaces = << <<" ">>
|| _ <- lists:seq(BS, 31)
>>,
<<Bytes/bytes, Spaces/bytes>>.
-spec listen(PortNum) -> Result -spec listen(PortNum) -> Result
when PortNum :: inet:port_num(), when PortNum :: inet:port_num(),
Result :: ok Result :: ok
@@ -42,6 +64,7 @@ ignore() ->
%% See: http://erlang.org/doc/apps/kernel/application.html %% See: http://erlang.org/doc/apps/kernel/application.html
start(normal, _Args) -> start(normal, _Args) ->
ok = application:ensure_started(hakuzaru),
Result = fd_sup:start_link(), Result = fd_sup:start_link(),
ok = listen(8000), ok = listen(8000),
Result. Result.
+618
View File
@@ -0,0 +1,618 @@
% @doc
% GRIDS library: grids
%
% This module simply handles encoding and decoding of GRIDS URLs.
%
% For documentation on GRIDS see
%
% https://git.qpq.swiss/QPQ-AG/research-megadoc/wiki/GRIDS
% @end
-module(gmgrids).
-vsn("0.2.0").
-author("Peter Harpending <peterharpending@qpq.swiss>").
-copyright("2025 QPQ AG").
-license("MIT").
% TODONE:
%
% TODO:
%
% - possibly input types should be iolists. I think for
% binaries it's fine
% - change the types... don't need 3 different record types
% FIXEDME:
%
% FIXME:
-export_type([
% field types
host/0,
target/0, akstr/0, pubkey/0,
% record types
grids/0
]).
-export([
% convenience functions
%% currency granularities
p/0, kp/0, mp/0, gp/0, tp/0, pp/0,
g/0, kg/0, mg/0, gg/0, tg/0,
%% type fuckery
target_to_path/1, akstr/1, unakstr/1,
dummy_target/0,
%% convenience encoders
encode/2,
% "primitives"
%% record constructor
mk_grids/2,
%% primitive encoders
encode/1, encode/7, percent_encode/1,
%% decoder
decode/1
]).
%%-------------------------------------------------------------------
%% API: TYPES
%%-------------------------------------------------------------------
% @doc Future-proofing... later this could be inet:addr() or whatever, or maybe
% {Host, Port}. Keeping it simple for now
-type host() :: string().
% @doc ak_... string
-type akstr() :: string().
% @doc 32-byte public key
-type pubkey() :: <<_:256>>.
% @doc later might want this to be flexible, "ak_..." etc
%
% FIXME: add support for all the different api keys: ak_..., ct_..., etc
-type target() :: pubkey()
| akstr().
-record(grids,
{secure = true :: boolean(),
host :: host(),
version = 1 :: integer(),
instruction :: dead_drop | spend | transfer,
path :: string(),
amount = none :: none | {value, integer()},
payload = none :: none | {value, binary()}}).
-type grids() :: #grids{}.
%%-------------------------------------------------------------------
%% API: CONVENIENCE FUNCTIONS
%%-------------------------------------------------------------------
%% currency granularities
p() -> 1.
kp() -> 1_000.
mp() -> 1_000_000.
gp() -> 1_000_000_000.
tp() -> 1_000_000_000_000.
pp() -> 1_000_000_000_000_000.
g() -> 1_000_000_000_000_000_000.
kg() -> 1_000_000_000_000_000_000_000.
mg() -> 1_000_000_000_000_000_000_000_000.
gg() -> 1_000_000_000_000_000_000_000_000_000.
tg() -> 1_000_000_000_000_000_000_000_000_000_000.
-spec target_to_path(Target) -> Path
when Target :: target(),
Path :: string().
% @doc
% Internal function exported for convenience purposes
%
% If `Target' is an "ak_..." string, leave as-is. If it's a 32 byte public key
% encode as an ak_... string
target_to_path(Target) ->
i_ttp(iolist_to_binary(Target)).
i_ttp(ApiStr = <<"ak_", _/binary>>) -> ApiStr;
i_ttp(Pubkey = <<_:32/bytes>>) -> akstr(Pubkey);
i_ttp(BadTarget) -> error({invalid_target, BadTarget}).
%% akstr/unakstr
-spec akstr(Pubkey) -> AkStr
when Pubkey :: pubkey(),
AkStr :: akstr().
% @doc
% convert a 32-byte public key into an ak_... string
akstr(PK) ->
unicode:characters_to_list(gmser_api_encoder:encode(account_pubkey, PK)).
-spec unakstr(AkStr) -> Pubkey
when Pubkey :: pubkey(),
AkStr :: string().
% @doc
% convert an ak_... string into a 32-byte public key
unakstr(Akstr) ->
{_, PK} = gmser_api_encoder:decode(unicode:characters_to_binary(Akstr)),
PK.
-spec dummy_target() -> akstr().
% @doc Make a dummy public key. For testing purposes. NOT secure!
dummy_target() ->
akstr(rand:bytes(32)).
-spec encode(Args, Options) -> URL
when Args :: {dead_drop, Host, Path}
| {spend, NetworkId, Recipient}
| {transfer, Host, Path},
Host :: iolist(),
Path :: iolist(),
NetworkId :: iolist(),
Recipient :: target(),
Options :: [Opt],
Opt :: {secure, boolean()}
| {version, integer()}
| {amount, Amount}
| {payload, Payload},
Amount :: integer() | none | {value, integer()},
Payload :: iolist() | none | {value, iolist()},
URL :: string().
encode(Args, Opts) ->
encode(mk_grids(Args, Opts)).
%%-------------------------------------------------------------------
%% API: RECORD CONSTRUCTORS
%%-------------------------------------------------------------------
-record(o,
{secure = true :: boolean(),
version = 1 :: integer(),
amount = none :: none | {value, integer()},
payload = none :: none | {value, binary()}}).
-spec mk_grids(Args, Options) -> Grids
when Args :: {dead_drop, Host, Path}
| {spend, NetworkId, Recipient}
| {transfer, Host, Path},
Host :: iolist(),
Path :: iolist(),
NetworkId :: iolist(),
Recipient :: target(),
Options :: [Opt],
Opt :: {secure, boolean()}
| {version, integer()}
| {amount, Amount}
| {payload, Payload},
Amount :: integer() | none | {value, integer()},
Payload :: iolist() | none | {value, iolist()},
Grids :: #grids{}.
mk_grids(Args, Options) ->
#o{secure = Secure,
version = Version,
amount = MaybeAmount,
payload = MaybePayload} = i_valid_opts(Options),
{Instruction, HostStr, Path} =
case Args of
{dead_drop, H0, P0} ->
H1 = unicode:characters_to_list(H0),
P1 = unicode:characters_to_list(P0),
{dead_drop, H1, P1};
{spend, NetId0, Recip0} ->
NetId1 = unicode:characters_to_list(NetId0),
Recip1 = target_to_path(Recip0),
{spend, NetId1, Recip1};
{transfer, H0, P0} ->
H1 = unicode:characters_to_list(H0),
P1 = unicode:characters_to_list(P0),
{transfer, H1, P1}
end,
#grids{secure = Secure,
host = HostStr,
version = Version,
instruction = Instruction,
path = Path,
amount = MaybeAmount,
payload = MaybePayload}.
i_valid_opts(Options) ->
Secure =
case proplists:get_value(secure, Options, true) of
S when is_boolean(S) -> S;
W -> error({invalid_option, {secure, W}})
end,
Version =
case proplists:get_value(version, Options, 1) of
V when is_integer(V) -> V;
X -> error({invalid_option, {version, X}})
end,
Amount =
case proplists:get_value(amount, Options, none) of
none -> none;
{value, N} when is_integer(N) -> {value, N};
N when is_integer(N) -> {value, N};
Y -> error({invalid_option, {amount, Y}})
end,
Payload =
case proplists:get_value(payload, Options, none) of
none -> none;
{value, P} -> {value, iolist_to_binary(P)};
P -> {value, iolist_to_binary(P)}
end,
#o{secure = Secure,
version = Version,
amount = Amount,
payload = Payload}.
%%-------------------------------------------------------------------
%% API: ENCODING (Record -> URL)
%%-------------------------------------------------------------------
-spec encode(GRIDS) -> URL
when GRIDS :: grids(),
URL :: string().
% @doc
% Encode a grids record type
% @end
encode(#grids{secure = Secure,
host = Host,
version = Vsn,
instruction = Instruction,
path = Path,
amount = Amt,
payload = Payload}) ->
encode(Secure, Host, Vsn, Instruction, Path, Amt, Payload).
-spec encode(Secure, Host, Version, Instruction, Path, Amount, Payload) -> URL
when Secure :: boolean(),
Host :: host(),
Version :: integer(),
Instruction :: dead_drop | spend | transfer,
Path :: string(),
Amount :: none | {value, integer()},
Payload :: none | {value, binary()},
URL :: string().
% @doc
% internal encode that's more verbose
encode(Secure, Host, Version, Instruction, Path, Amount, Payload) ->
unicode:characters_to_list(
["grid", i_encode_secure(Secure),
"://", i_encode_host(Host),
"/", integer_to_list(Version),
"/", i_encode_instruction(Instruction),
"/", Path,
i_encode_qstr(Amount, Payload)]
).
i_encode_secure(true) -> "s";
i_encode_secure(false) -> "".
% future-proofing against more complicated host arguments
i_encode_host(Host) -> Host.
i_encode_instruction(dead_drop) -> "d";
i_encode_instruction(spend) -> "s";
i_encode_instruction(transfer) -> "t".
i_encode_qstr(none, none) ->
"";
i_encode_qstr({value, Amt}, none) ->
["?a=", integer_to_list(Amt)];
i_encode_qstr(none, {value, Payload}) ->
["?p=", percent_encode(Payload)];
i_encode_qstr({value, Amt}, {value, Payload}) ->
["?a=", integer_to_list(Amt),
"&p=", percent_encode(Payload)].
-spec percent_encode(Payload) -> PercentEncoded
when Payload :: binary(),
PercentEncoded :: iolist().
% @doc
% internal function to percent-encode binary payload
% exported for convenience
%
% See: https://en.wikipedia.org/wiki/Percent-encoding
percent_encode(Payload) when is_binary(Payload) ->
i_percent_encode(Payload, []).
% unreserved characters
i_percent_encode(<<C:8, Rest/binary>>, Acc)
when ($A =< C andalso C =< $Z) orelse
($a =< C andalso C =< $z) orelse
($0 =< C andalso C =< $9) orelse
(C =:= $-) orelse
(C =:= $_) orelse
(C =:= $~) orelse
(C =:= $.) ->
i_percent_encode(Rest, [Acc, C]);
i_percent_encode(<<B:8, Rest/binary>>, Acc) ->
i_percent_encode(Rest, [Acc, i_pe_byte(B)]);
i_percent_encode(<<>>, Result) ->
Result.
% single hex digit
i_pe_byte(B) when 16#00 =< B, B =< 16#0F -> ["%0", integer_to_list(B, 16)];
i_pe_byte(B) when 16#10 =< B, B =< 16#FF -> ["%", integer_to_list(B, 16)].
%%---------------------------------------------------------
%% API: DECODING
%%---------------------------------------------------------
-record(dt,
{secure = undefined :: undefined | boolean(),
host = undefined :: undefined | iolist(),
version = undefined :: undefined | integer(),
instruction = undefined :: undefined | spend | transfer | dead_drop,
path = undefined :: undefined | iolist(),
amount = undefined :: undefined | none | {value, integer()},
payload = undefined :: undefined | none | {value, binary()}}).
% -type decode_target() :: #dt{}.
-spec decode(URL) -> Result
when URL :: string(),
Result :: {ok, grids(), Remainder :: string()}
| {error, Reason},
Reason :: term().
decode(URL) ->
case i_decode(unicode:characters_to_binary(URL)) of
{ok, DT, Remainder} ->
{ok, i_decode_dt(DT), unicode:characters_to_list(Remainder)};
Error ->
Error
end.
i_decode_dt(#dt{secure = S,
host = H,
version = V,
path = P,
instruction = Instruction,
amount = A,
payload = L}) ->
HStr = unicode:characters_to_list(H),
PStr = unicode:characters_to_list(P),
#grids{secure = S,
host = HStr,
version = V,
path = PStr,
instruction = Instruction,
amount = A,
payload = L}.
i_decode(URL) ->
i_pipeline([fun i_decode_secure/2,
fun i_decode_host/2,
fun i_decode_version/2,
fun i_decode_instruction/2,
fun i_decode_path/2,
fun i_decode_qstr/2],
{ok, #dt{}, URL}).
i_pipeline([Fun | Funs], Acc) ->
case Acc of
{ok, DT, URL} -> i_pipeline(Funs, Fun(DT, URL));
Error -> Error
end;
i_pipeline([], Result) ->
Result.
i_decode_secure(DT = #dt{secure = undefined},
<<"grid://", Rest/binary>>) ->
{ok, DT#dt{secure = false}, Rest};
i_decode_secure(DT = #dt{secure = undefined},
<<"grids://", Rest/binary>>) ->
{ok, DT#dt{secure = true}, Rest};
i_decode_secure(_, URL) ->
{error, {bad_protocol, URL}}.
i_decode_host(DT = #dt{host = undefined}, URL) ->
case idh2([], URL) of
{ok, Host, Rest} -> {ok, DT#dt{host = Host}, Rest};
Error -> Error
end.
% eliminate empty hosts and hosts not followed by /
idh2([], <<$/:8, _/binary>>) -> {error, empty_host};
idh2([], <<>>) -> {error, empty_host};
idh2(Host, <<>>) -> {error, {bad_host, Host}};
idh2(Host, <<$/:8, Rest/binary>>) -> {ok, Host, Rest};
idh2(Acc, <<Char:8, Rest/binary>>) -> idh2([Acc, Char], Rest).
i_decode_version(DT = #dt{version = undefined}, URL) ->
case idv2([], URL) of
{ok, VStr, Rest} ->
Version = list_to_integer(unicode:characters_to_list(VStr)),
NewDT = DT#dt{version = Version},
{ok, NewDT, Rest};
Error ->
Error
end.
idv2([], <<$/:8, _/binary>>) -> {error, empty_host};
idv2([], <<>>) -> {error, empty_host};
idv2(Vstr, <<>>) -> {error, {bad_version, iolist_to_binary(Vstr)}};
idv2(Vstr, <<$/:8, Rest/binary>>) -> {ok, Vstr, Rest};
idv2(Acc, <<N:8, Rest/binary>>) ->
case ($0 =< N) andalso (N =< $9) of
true -> idv2([Acc, N], Rest);
false -> {error, {illegal_version_char, [N]}}
end.
i_decode_instruction(DT = #dt{instruction = undefined}, <<"s/", Rest/binary>>) ->
{ok, DT#dt{instruction = spend}, Rest};
i_decode_instruction(DT = #dt{instruction = undefined}, <<"t/", Rest/binary>>) ->
{ok, DT#dt{instruction = transfer}, Rest};
i_decode_instruction(DT = #dt{instruction = undefined}, <<"d/", Rest/binary>>) ->
{ok, DT#dt{instruction = dead_drop}, Rest};
i_decode_instruction(_ = #dt{instruction = undefined}, Bad) ->
{error, {illegal_instruction, Bad}}.
i_decode_path(DT = #dt{path = undefined}, URL) ->
{Path, Rest} = idp([], URL),
{ok, DT#dt{path = Path}, Rest}.
% consume until we get to end of string or ?
idp(Path, <<"?", Rest/binary>>) -> {Path, Rest};
idp(Path, <<>>) -> {Path, <<>>};
idp(Path, <<C:8, Rest/binary>>) -> idp([Path, C], Rest).
i_decode_qstr(DT = #dt{amount = undefined, payload = undefined}, URL) ->
case idq([], URL) of
{ok, Proplist, Remainder} ->
Amount = proplists:get_value(amount, Proplist, none),
Payload = proplists:get_value(payload, Proplist, none),
NewDT = DT#dt{amount = Amount, payload = Payload},
{ok, NewDT, Remainder};
Error ->
Error
end.
-spec idq(Proplist, URL) -> Result
when URL :: binary(),
Result :: {ok, Proplist, Remainder}
| {error, ParseError},
Proplist :: [Prop],
Prop :: {amount, {value, integer()}}
| {payload, {value, binary()}},
ParseError :: any(),
Remainder :: binary().
idq(Params, <<"a=", Rest/binary>>) ->
case i_parse_amt(none, Rest) of
{ok, Amt, NewRest} ->
NewParams = [{amount, {value, Amt}} | Params],
idq(NewParams, NewRest);
Error ->
Error
end;
idq(Params, <<"p=", Rest/binary>>) ->
case i_parse_payload(none, Rest) of
{ok, Payload, NewRest} ->
NewParams = [{payload, {value, Payload}} | Params],
idq(NewParams, NewRest);
Error ->
Error
end;
idq(Params, Rest) ->
{ok, Params, Rest}.
-spec i_parse_amt(MaybeAmount, URL) -> Result
when URL :: binary(),
Result :: {ok, MaybeAmount, Rest}
| {error, term()},
MaybeAmount :: none | {value, integer()},
Rest :: binary().
% @private context here is we have an a= and we're parsing what comes after
% that
%
% we can error on empty amounts
i_parse_amt(Acc, <<DigitChar:8, Rest/binary>>)
when $0 =< DigitChar, DigitChar =< $9 ->
DigitInt = DigitChar - $0,
NewAcc =
case Acc of
none -> {value, DigitInt};
{value, N} -> {value, N*10 + DigitInt}
end,
i_parse_amt(NewAcc, Rest);
% either end of string or non-digit char
i_parse_amt(Acc, <<"&", Rest/binary>>) ->
case Acc of
{value, Amount} -> {ok, Amount, Rest};
none -> {error, empty_amount}
end;
i_parse_amt(Acc, Rest) ->
case Acc of
{value, Amount} -> {ok, Amount, Rest};
none -> {error, empty_amount}
end.
-define(IS_HEX_CHAR(P), ((($0 =< P) andalso (P =< $9)) orelse
(($A =< P) andalso (P =< $F)))).
-spec i_parse_payload(MaybePayload, URL) -> Result
when URL :: binary(),
Result :: {ok, MaybePayload, Rest}
| {error, term()},
MaybePayload :: none | {value, binary()},
Rest :: binary().
% unreserved chars
i_parse_payload(Acc, <<C:8, Rest/binary>>)
when ($A =< C andalso C =< $Z) orelse
($a =< C andalso C =< $z) orelse
($0 =< C andalso C =< $9) orelse
(C =:= $-) orelse
(C =:= $_) orelse
(C =:= $~) orelse
(C =:= $.) ->
NewAcc =
case Acc of
none -> {value, <<C:8>>};
{value, Bytes} -> {value, <<Bytes/bytes, C:8>>}
end,
i_parse_payload(NewAcc, Rest);
% percent char
i_parse_payload(Acc, <<"%", A:8, B:8, Rest/binary>>)
when ?IS_HEX_CHAR(A), ?IS_HEX_CHAR(B) ->
AInt = list_to_integer([A], 16),
BInt = list_to_integer([B], 16),
NewByte = AInt*16 + BInt,
NewAcc =
case Acc of
none -> {value, <<NewByte:8>>};
{value, Bytes} -> {value, <<Bytes/binary, NewByte:8>>}
end,
i_parse_payload(NewAcc, Rest);
% random char
i_parse_payload(Acc, <<"&", Rest/binary>>) ->
case Acc of
none -> {error, empty_payload};
{value, Payload} -> {ok, Payload, Rest}
end;
i_parse_payload(Acc, Rest) ->
case Acc of
none -> {error, {illegal_payload, Rest}};
{value, Payload} -> {ok, Payload, Rest}
end.
+12 -1
View File
@@ -5,7 +5,18 @@
{prefix,"fd"}. {prefix,"fd"}.
{desc,"Front End Web Dev in Erlang stuff"}. {desc,"Front End Web Dev in Erlang stuff"}.
{package_id,{"otpr","fewd",{0,2,0}}}. {package_id,{"otpr","fewd",{0,2,0}}}.
{deps,[{"otpr","zj",{1,1,2}}]}. {deps,[{"otpr","hakuzaru",{0,8,3}},
{"otpr","sophia",{9,0,0}},
{"otpr","gmserialization",{0,1,3}},
{"otpr","eblake2",{1,0,1}},
{"otpr","base58",{0,1,1}},
{"otpr","gmbytecode",{3,4,1}},
{"otpr","base58",{0,1,1}},
{"otpr","eblake2",{1,0,1}},
{"otpr","ec_utils",{1,0,0}},
{"otpr","zj",{1,1,2}},
{"otpr","getopt",{1,0,2}},
{"otpr","qr",{0,1,0}}]}.
{key_name,none}. {key_name,none}.
{a_email,"peterharpending@qpq.swiss"}. {a_email,"peterharpending@qpq.swiss"}.
{c_email,"peterharpending@qpq.swiss"}. {c_email,"peterharpending@qpq.swiss"}.