From 60803b4a4ec4e7d6b52373213c3d0d2b220a95a1 Mon Sep 17 00:00:00 2001 From: Peter Harpending Date: Fri, 19 Dec 2025 17:47:31 -0800 Subject: [PATCH] start grids qr demo --- src/fd_gridsd.erl | 122 +++++++++ src/fd_sup.erl | 8 +- src/fewd.erl | 14 ++ src/gmgrids.erl | 618 ++++++++++++++++++++++++++++++++++++++++++++++ zomp.meta | 4 +- 5 files changed, 764 insertions(+), 2 deletions(-) create mode 100644 src/fd_gridsd.erl create mode 100644 src/gmgrids.erl diff --git a/src/fd_gridsd.erl b/src/fd_gridsd.erl new file mode 100644 index 0000000..6b53b00 --- /dev/null +++ b/src/fd_gridsd.erl @@ -0,0 +1,122 @@ +% @doc grids cache +-module(fd_gridsd). +-vsn("0.2.0"). + +-behavior(gen_server). + +-export_type([ +]). + +-export([ + %% caller context + get_url/2, + + %% 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 mh_str() :: string(). + +%% for craig's autism +-record(sp, + {recipient :: string(), + amount :: non_neg_integer(), + payload :: binary()}). + +-type search_pattern() :: #sp{}. + +-record(s, + {current_gen_height :: pos_integer(), + current_gen_hash :: string(), + current_gen_seen_mb_hashes :: [mh_str()], + past_gen_seen_mb_hashes :: [mh_str()], + looking_for :: [search_pattern()]}). +-type state() :: #s{}. + + +%%----------------------------------------------------------------------------- +%% caller context +%%----------------------------------------------------------------------------- + +-spec get_url(Amount, Payload) -> {ok, URL, QR_PNG} | {error, term()} + when Amount :: none | pos_integer(), + Payload :: none | binary(), + URL :: string(), + QR_PNG :: binary(). + +get_url(Amount, Payload) -> + gen_server:call(?MODULE, {get_url, 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({get_url, Amount, Payload}, From, State) -> + case i_get_url(Amount, Payload, From, 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 i_get_url(Amount, Payload, From, State) -> Result + when Amount :: none | pos_integer(), + Payload :: none | binary(), + From :: {pid(), reference()}, + State :: state(), + URL :: string(), + QR_PNG :: binary(). + Result :: {ok, URL, QR_PNG, NewState} + | {error, term()}. + +i_get_url(Amount, Payload, From, State) -> + NetworkId = fewd:network_id(), + Pubkey = fewd:pubkey(), + URL = gmgrids:encode({spend, NetworkId, Pubkey}, + [{amount, Amount}, {payload, Payload}]), + diff --git a/src/fd_sup.erl b/src/fd_sup.erl index 6e16f69..3a31653 100644 --- a/src/fd_sup.erl +++ b/src/fd_sup.erl @@ -36,6 +36,12 @@ start_link() -> init([]) -> RestartStrategy = {one_for_one, 1, 60}, + GridsD = {fd_gridsd, + {fd_gridsd, start_link, []}, + permanent, + 5000, + worker, + [fd_gridsd]}, WFCd = {fd_wfcd, {fd_wfcd, start_link, []}, permanent, @@ -48,5 +54,5 @@ init([]) -> 5000, supervisor, [fd_httpd]}, - Children = [WFCd, Httpd], + Children = [GridsD, WFCd, Httpd], {ok, {RestartStrategy, Children}}. diff --git a/src/fewd.erl b/src/fewd.erl index 7461ef3..cc74f93 100644 --- a/src/fewd.erl +++ b/src/fewd.erl @@ -9,12 +9,25 @@ -copyright("Peter Harpending "). -license("BSD-2-Clause-FreeBSD"). +-export([network_id/0, pubkey/0]). -export([listen/1, ignore/0]). -export([start/2, stop/1]). -include("$zx_include/zx_logger.hrl"). +network_id() -> "groot.testnet". +pubkey() -> pad32("fewd demo"). + + +pad32(Bytes) -> + BS = byte_size(Bytes), + Spaces = << <<" ">> + || _ <- lists:seq(BS, 31) + >>, + <>. + + -spec listen(PortNum) -> Result when PortNum :: inet:port_num(), Result :: ok @@ -42,6 +55,7 @@ ignore() -> %% See: http://erlang.org/doc/apps/kernel/application.html start(normal, _Args) -> + ok = application:ensure_started(hakuzaru), Result = fd_sup:start_link(), ok = listen(8000), Result. diff --git a/src/gmgrids.erl b/src/gmgrids.erl new file mode 100644 index 0000000..85a42ec --- /dev/null +++ b/src/gmgrids.erl @@ -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 "). +-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(<>, 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(<>, 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, <>) -> 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, <>) -> + 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, <>) -> 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, <>) + 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, <>) + 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, <>}; + {value, Bytes} -> {value, <>} + 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, <>}; + {value, Bytes} -> {value, <>} + 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. diff --git a/zomp.meta b/zomp.meta index 5230b37..24e5499 100644 --- a/zomp.meta +++ b/zomp.meta @@ -5,7 +5,9 @@ {prefix,"fd"}. {desc,"Front End Web Dev in Erlang stuff"}. {package_id,{"otpr","fewd",{0,2,0}}}. -{deps,[{"otpr","qr",{0,1,0}},{"otpr","zj",{1,1,2}}]}. +{deps,[{"otpr","hakuzaru",{0,7,0}}, + {"otpr","qr",{0,1,0}}, + {"otpr","zj",{1,1,2}}]}. {key_name,none}. {a_email,"peterharpending@qpq.swiss"}. {c_email,"peterharpending@qpq.swiss"}.