%%% @doc %%% GRIDS URL parsing %%% %%% GRID(S): Gajumaru Remote Instruction Dispatch (Serialization) %%% GRIDS is a Gajumaru protocol for encoding wallet instructions as URLs. %%% Version 1 of the protocol consists of two verbs with two contexts each, collapsed to %%% four symbols for brevity. %%% %%% The GRIDS schema begins with "grids://" or "grid://" %%% Which way this is interpreted can vary depending on the verb. %%% %%% The typical "host" component is either an actual hostname or address and an optional %%% port number (the defaut port being 3013), or a Gajumaru chain network IDi (in which %%% case the port number is ignored if provided). Which way this field is interpreted %%% depends on the verb. %%% %%% The first element of the path after the host component indicates the protocol version. %%% Only version 1 exists at the time of this release. %%% %%% The next element of the path after the version is a single letter that indicates which %%% action to take. The following actions are available: %%% "s": Spend on Chain %%% Constructs a spend transaction to the address indicated in the path component %%% indicated in the final path element. Two qargs are valid in the trailing arguments %%% section: "a" for amount (in Pucks, not Gajus!), and "p" for data payload. %%% In this context the "host" field in the URL is interpreted as a chain network ID. %%% "t": Transfer (spend) on Host %%% The same as "spend" above, but in this context the host field of the URL is %%% interpreted as host[:port] information and the network chain ID that will be used %%% will be derived from whatever chain the given host reports. %%% "d": Dead-drop signature request %%% This instructs the wallet to retrieve a signature data blob from an HTTP or HTTPS %%% URL that can be reconstructed by replacing "grids" with "https" or "grid" with %%% "http", omitting the "/1/d" path component and then recnstructing the URL. %%% This provides a lightweight method for services to enable contract calls from %%% wallets that are not capable of compiling contract source. %%% @end -module(hz_grids). -vsn("0.8.2"). -export([url/2, url/3, url/4, parse/1, req/2, req/3]). -spec url(Instruction, HTTP) -> Result when Instruction :: spend | transfer | sign, HTTP :: uri_string:uri_string(), Result :: {ok, GRIDS} | uri_string:uri_error(), GRIDS :: uri_string:uri_string(). %% @doc %% Takes url(Instruction, HTTP) -> case uri_string:parse(HTTP) of U = #{scheme := "https"} -> url2(Instruction, U#{scheme := "grids"}); U = #{scheme := "http"} -> url2(Instruction, U#{scheme := "grid"}); Error -> Error end. url2(Instruction, URL = #{path := Path}) -> GRIDS = case Instruction of spend -> URL#{path := "/1/s" ++ Path}; transfer -> URL#{path := "/1/t" ++ Path}; sign -> URL#{path := "/1/d" ++ Path} end, {ok, uri_string:recompose(GRIDS)}. -spec url(Instruction, Recipient, Amount) -> GRIDS when Instruction :: {spend, Network} | {transfer, Node}, Network :: string(), Node :: {inet:ip_address() | inet:hostname(), inet:port_number()} | uri_string:uri_string(), Recipient :: string(), Amount :: non_neg_integer(), GRIDS :: uri_string:uri_string(). %% @doc %% Forms a GRIDS URL for spends or transfers. %% @equiv uri(Instruction, Recipient, Amount, "") url(Instruction, Recipient, Amount) -> url(Instruction, Recipient, Amount, ""). -spec url(Instruction, Recipient, Amount, Payload) -> GRIDS when Instruction :: {spend, Network} | {transfer, Node}, Network :: string(), Node :: {inet:ip_address() | inet:hostname(), inet:port_number()} | uri_string:uri_string(), % "http://..." | "https://..." Recipient :: string(), Amount :: non_neg_integer() | none, Payload :: binary(), GRIDS :: uri_string:uri_string(). %% @doc %% Forms a GRIDS URL for spends or transfers. url({spend, Network}, Recipient, Amount, Payload) -> Elements = ["grids://", Network, "/1/s/", Recipient, qwargs(Amount, Payload)], unicode:characters_to_list(Elements); url({transfer, Node}, Recipient, Amount, Payload) -> Prefix = case Node of {H, P} -> ["grid://", h_to_s(H), ":", integer_to_list(P)]; "https://" ++ H -> ["grids://", H]; "http://" ++ H -> ["grid://", H]; <<"https://", H/binary>> -> ["grids://", H]; <<"http://", H/binary>> -> ["grid://", H] end, unicode:characters_to_list([Prefix, "/1/t/", Recipient, qwargs(Amount, Payload)]). h_to_s(Host) when is_list(Host) -> Host; h_to_s(Host) when is_binary(Host) -> Host; h_to_s(Host) when is_tuple(Host) -> inet:ntoa(Host); h_to_s(Host) when is_atom(Host) -> atom_to_list(Host). qwargs(none, "") -> []; qwargs(Amount, "") -> ["?a=", integer_to_list(Amount)]; qwargs(none, Payload) -> [$? | uri_string:compose_query([{"p", Payload}])]; qwargs(Amount, Payload) -> [$? | uri_string:compose_query([{"a", integer_to_list(Amount)}, {"p", Payload}])]. -spec parse(GRIDS) -> Result when GRIDS :: string(), Result :: {ok, Instruction} | uri_string:error(), Instruction :: {{spend, chain | node}, {Location, Recipient, Amount, Payload}} | {{sign, http | https}, URL}, Location :: Node :: {inet:ip_address() | inet:hostname(), inet:port_number()} | Chain :: binary(), Recipient :: gajudesk:id(), Amount :: non_neg_integer(), Payload :: binary(), URL :: string(). parse(GRIDS) -> case uri_string:parse(GRIDS) of #{path := "/1/s/" ++ R, host := H, query := Q, scheme := "grids"} -> spend(R, chain, list_to_binary(H), Q); #{path := "/1/s/" ++ R, host := H, query := Q, scheme := "grid"} -> spend(R, chain, list_to_binary(H), Q); #{path := "/1/t/" ++ R, host := H, port := P, query := Q, scheme := "grids"} -> spend(R, node, {H, P}, Q); #{path := "/1/t/" ++ R, host := H, port := P, query := Q, scheme := "grid"} -> spend(R, node, {H, P}, Q); #{path := "/1/t/" ++ R, host := H, query := Q, scheme := "grids"} -> spend(R, node, {H, 3013}, Q); #{path := "/1/t/" ++ R, host := H, query := Q, scheme := "grid"} -> spend(R, node, {H, 3013}, Q); U = #{path := "/1/d/" ++ L, scheme := "grids"} -> HTTP = uri_string:recompose(U#{scheme := "https", path := L}), {ok ,{{sign, https}, HTTP}}; U = #{path := "/1/d/" ++ L, scheme := "grid"} -> HTTP = uri_string:recompose(U#{scheme := "http", path := L}), {ok, {{sign, http}, HTTP}}; Error -> Error end. spend(Recipient, Context, Location, Qwargs) -> case dissect_query(Qwargs) of {ok, Amount, Payload} -> {ok, {{spend, Context}, {Location, Recipient, Amount, Payload}}}; Error -> Error end. dissect_query(Qwargs) -> case uri_string:dissect_query(Qwargs) of {error, Reason, Info} -> {error, Reason, Info}; ArgList -> case l_to_i(proplists:get_value("a", ArgList, "0")) of {ok, Amount} -> Payload = list_to_binary(proplists:get_value("p", ArgList, "")), {ok, Amount, Payload}; Error -> Error end end. l_to_i(S) -> try {ok, list_to_integer(S)} catch error:badarg -> {error, bad_url} end. req(Type, Message) -> req(Type, Message, false). req(sign, Message, ID) -> #{"grids" => 1, "chain" => "gajumaru", "network_id" => hz:network_id(), "type" => "message", "public_id" => ID, "payload" => Message}; req(tx, Data, ID) -> #{"grids" => 1, "chain" => "gajumaru", "network_id" => hz:network_id(), "type" => "tx", "public_id" => ID, "payload" => Data}; req(ack, Message, ID) -> #{"grids" => 1, "chain" => "gajumaru", "network_id" => hz:network_id(), "type" => "ack", "public_id" => ID, "payload" => Message}.