Compare commits
137 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bb7b4c3629 | |||
| 11a9b36681 | |||
| 75bc52ede3 | |||
| 29619f08b7 | |||
| af46223163 | |||
| 9cafdd2b0f | |||
| 6d429aa6a4 | |||
| fcf85077b2 | |||
| 3585dbe534 | |||
| 9a7a2a98c4 | |||
| 4530fd2e93 | |||
| 2a7079129f | |||
| 88aeb39d4a | |||
| 9fc89c0c22 | |||
| 23c13f607e | |||
| 8bc79d3b3f | |||
| 3fae9a2edd | |||
| a3b19747b6 | |||
| f8e9333b4b | |||
| eaccd50764 | |||
| 9fd8dbd1a6 | |||
| f0f86ed36d | |||
| ed252b4c06 | |||
| 5dcc05d56a | |||
| 2eca3a5338 | |||
| e595991616 | |||
| da92d80334 | |||
| f821d57c1c | |||
| e87be689a8 | |||
| 2a7de4fee1 | |||
| 82d08da8ca | |||
| 85d0c6fd04 | |||
| d8221e0b25 | |||
| b950bb8a67 | |||
| a4914c1ad1 | |||
| 9e6d9ec02e | |||
| 4b9fa65672 | |||
| 74aaad297a | |||
| c9ead44aa2 | |||
| c54c0db17a | |||
| cd4f6a56a5 | |||
| fd2158a465 | |||
| 7fc3cd00da | |||
| 02945dd10d | |||
| 695e7e4828 | |||
| 9f02f73dbd | |||
| fd8766a249 | |||
| 540b2c513b | |||
| bda4e89e58 | |||
| f277e79096 | |||
| ddec3bfa74 | |||
| a0fbeebcdb | |||
| 78c9c67f38 | |||
| 9bc0ffafd1 | |||
| a1fc5f19fa | |||
| efe0a64056 | |||
| 60985130cb | |||
| 6c172c4783 | |||
| 3838a7e3c5 | |||
| d014ae0982 | |||
| bb4bcbb7de | |||
| a695c21fc9 | |||
| 493bdb990c | |||
| 17f635af61 | |||
| 272ed01fdc | |||
| 49cd8b6687 | |||
| 966b4b2748 | |||
| fe182a5233 | |||
| f1696e2b9e | |||
| 2bf384ca82 | |||
| 4f2a3c6c6f | |||
| 7df04a81be | |||
| 6f02d4c4e6 | |||
| 48bcccdf23 | |||
| 03b9756066 | |||
| 56e63051bc | |||
| 3f1c9bd626 | |||
| 97e32574c4 | |||
| 6f5525afcf | |||
| 4f1958b210 | |||
| 3da9bd570b | |||
| d65a048409 | |||
| 9280495b18 | |||
| d2163c1ff8 | |||
| af1639d47b | |||
| cb36bad74b | |||
| 17a2b867fe | |||
| a35118db7a | |||
| b719c946ef | |||
| 8b6085dee8 | |||
| 60c8adb4b1 | |||
| ebb84b39a1 | |||
| e456a96f52 | |||
| 2f93c4d503 | |||
| b542205c0e | |||
| 6ba0c1f2ae | |||
| d1cae68ce7 | |||
| ccd9a8c83d | |||
| e10ef8a460 | |||
| 79eb881208 | |||
| 9ac442ffb4 | |||
| 401f8dec7a | |||
| 81fef99d9c | |||
| 23137a677e | |||
| b219d0f784 | |||
| f5e955b583 | |||
| 61984d1529 | |||
| 1978ca59b3 | |||
| 4ee6609111 | |||
| aeb78eab38 | |||
| 88c6f6dcc7 | |||
| d5ff77b278 | |||
| 5a145207da | |||
| 11730de24a | |||
| 975325db14 | |||
| beed46a38b | |||
| e4e6e35bf8 | |||
| 7393a02de2 | |||
| c713053efd | |||
| 751c099a44 | |||
| be0607f7c1 | |||
| 823291986e | |||
| c5349f5736 | |||
| 7252ecd40b | |||
| e8febcf8d5 | |||
| 8a42f4a7a3 | |||
| a305bf3511 | |||
| f2fa83c215 | |||
| 4c09490f8a | |||
| 39b92996aa | |||
| d23196e746 | |||
| e9b1bccf57 | |||
| c9cdedf85c | |||
| 11516cb177 | |||
| 7c2db6eab7 | |||
| f770bc299e | |||
| c934510859 |
+2
-2
@@ -8,9 +8,9 @@ cancer
|
||||
erl_crash.dump
|
||||
ebin/*.beam
|
||||
doc/*.html
|
||||
doc/*.css
|
||||
doc/edoc-info
|
||||
doc/erlang.png
|
||||
doc/stylesheet.css
|
||||
doc/edoc-info
|
||||
rel/example_project
|
||||
.concrete/DEV_MODE
|
||||
.rebar
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 3.3 KiB |
@@ -0,0 +1,75 @@
|
||||
/* standard EDoc style sheet */
|
||||
body {
|
||||
font-family: Verdana, Arial, Helvetica, sans-serif;
|
||||
margin-left: .25in;
|
||||
margin-right: .2in;
|
||||
margin-top: 0.2in;
|
||||
margin-bottom: 0.2in;
|
||||
color: #696969;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
a:link{
|
||||
color: #000000;
|
||||
}
|
||||
a:visited{
|
||||
color: #000000;
|
||||
}
|
||||
a:hover{
|
||||
color: #d8613c;
|
||||
}
|
||||
h1,h2 {
|
||||
margin-left: -0.2in;
|
||||
}
|
||||
div.navbar {
|
||||
background-color: #000000;
|
||||
padding: 0.2em;
|
||||
}
|
||||
h2.indextitle {
|
||||
padding: 0.4em;
|
||||
color: #dfdfdf;
|
||||
background-color: #000000;
|
||||
}
|
||||
div.navbar a:link {
|
||||
color: #dfdfdf;
|
||||
}
|
||||
div.navbar a:visited {
|
||||
color: #dfdfdf;
|
||||
}
|
||||
div.navbar a:hover {
|
||||
color: #d8613c;
|
||||
}
|
||||
h3.function,h3.typedecl {
|
||||
background-color: #000000;
|
||||
color: #dfdfdf;
|
||||
padding-left: 1em;
|
||||
}
|
||||
div.spec {
|
||||
margin-left: 2em;
|
||||
background-color: #eeeeee;
|
||||
}
|
||||
a.module {
|
||||
text-decoration:none
|
||||
}
|
||||
a.module:hover {
|
||||
background-color: #eeeeee;
|
||||
}
|
||||
ul.definitions {
|
||||
list-style-type: none;
|
||||
}
|
||||
ul.index {
|
||||
list-style-type: none;
|
||||
background-color: #eeeeee;
|
||||
}
|
||||
|
||||
/*
|
||||
* Minor style tweaks
|
||||
*/
|
||||
ul {
|
||||
list-style-type: square;
|
||||
}
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
td {
|
||||
padding: 3
|
||||
}
|
||||
+60
-57
@@ -1,77 +1,80 @@
|
||||
@author Craig Everett <ceverett@zxq9.com> [https://gitlab.com/zxq9/zj]
|
||||
@version 0.3.0
|
||||
@title Vanillae: Aeternity blockchain bindings for Erlang
|
||||
@author Craig Everett <craigeverett@qpq.swiss> [https://zxq9.com]
|
||||
@author Jarvis Carrol <jarviscarrol@qpq.swiss> [https://jarviscarroll.net/]
|
||||
@version 0.9.2
|
||||
@title Hakuzaru: Gajumaru blockchain bindings for Erlang
|
||||
|
||||
@doc
|
||||
This Erlang application provides bindings for the Erlang blockchain.
|
||||
The primary goal is for usage to be easy to understand and as simple as possible to use.
|
||||
The secondary goal is to enable real-world projects to more easily connect with the blockchain in an obvious way and provide a clear path for them to provide feedback regarding areas that are difficult to understand, functionality that is lacking, and explain their use cases to us so we can more easily provide needed features and usage examples to make adoption easier.
|
||||
This Erlang application provides bindings for the Gajumaru blockchain and basic utilities for manipulating Gajumaru-related data.
|
||||
|
||||
== Basic operation ==
|
||||
All external interfaces expected to be used by authors of programs that use Vanillae are built into the `vanillae' module.
|
||||
== To Start or Not To Start ==
|
||||
Starting the `hakuzaru' application is only required if you need to query the chain.
|
||||
|
||||
When Vanillae is started as an application a named process called `vanillae_man' is spawned that manages interactions with and the state of the service, as well as a simple-one-for-one supervisor that manages the lifecycle of Vanillae workers (defined in `vanillae_fetcher').
|
||||
The application can be started via a call to `application', or with an explicit call to `hz:start()'.
|
||||
|
||||
After startup `vanillae_man' must be given the address and port of a list of Aeternity nodes that are available to service requests. Note that the service nodes will need to have the "dry run" endpoint enabled and the internal service query port made available in order to provide "dry run" and mempool TX submission functionality.
|
||||
Hakuzaru can also be run as a local application from the shell by invoking it with `zxh run hakuzaru' if you have `zx' installed.
|
||||
|
||||
The `vanillae_man' will round-robin requests to however many Aeternity nodes are provided in its configuration. Note that this congiruation is dynamic and can be changed completely at runtime.
|
||||
== Operation ==
|
||||
All blockchain-specific operations are accessible from the main interface modulle: `hz'
|
||||
|
||||
== Functions ==
|
||||
The `vanillae' module exposes one function per blockchain feature provided. Most of these are actually wrappers for blockchain endpoint functions, others provide functionality specific to accomplishing a local processing task related to chain data.
|
||||
When Hakuzaru is started as an application a named process called `hz_man' is spawned that manages interactions with chain nodes, as well as a simple-one-for-one supervisor that manages the lifecycle of workers (defined in `hz_fetcher').
|
||||
|
||||
== Initialization ==
|
||||
When Vanillae is first started the vanillae_man is started but does not yet know what Aeternity nodes to use to service queries. You will need to provide it with at least one node and port where it can make Aeternity endpoint calls.
|
||||
After startup `hz_man' must be given the address and port of a list of Gajumaru nodes that are available to service requests.
|
||||
Note that the service nodes will need to have the dry-run endpoint enabled and the internal service query port made available in order to provide dry-runs and transaction submission.
|
||||
|
||||
Note that if you will need to make read-only calls to contracts that are deployed on chain (to queery their state or perform specific read-only operations provided by the contract) the backend nodes you configure will need to be configured with "dry-run" enabled.
|
||||
When configuring chain nodes a list of nodes should be provided.
|
||||
To avoid sync issues in the case of fast transaction formation/submission to the chain, only one node from the list of chain nodes is used for submitting transactions and querying `next_nonce/1'.
|
||||
This node is called "the sticky node".
|
||||
|
||||
Example of a shell session where vanillae is started and initialized manually with an AE node in the local network at 192.168.10.10:3013:
|
||||
The first node in the list of chain nodes provided during configuration is designated as the sticky node.
|
||||
If you also want to use the sticky node as a query endpoint, include it twice in the list.
|
||||
|
||||
The `hz_man' will round-robin requests to however many additional Gajumaru nodes are provided in the configuration.
|
||||
Note that this configuration is dynamic and can be changed at runtime, so your service can adapt to node availability on the fly if needed.
|
||||
|
||||
```
|
||||
1> vanillae:start().
|
||||
Starting.
|
||||
ceverett@steak:~$ zxh run hakuzaru
|
||||
Erlang/OTP 27 [erts-15.2] [source] [64-bit] [smp:16:16] [ds:16:16:10] [async-threads:1] [jit:ns]
|
||||
|
||||
Eshell V15.2 (press Ctrl+G to abort, type help(). for help)
|
||||
Fetching otpr-hakuzaru-0.8.0
|
||||
[100.00%]
|
||||
Recompile: src/hz_sup
|
||||
Recompile: src/hz_man
|
||||
Recompile: src/hz_key_master
|
||||
Recompile: src/hz_grids
|
||||
Recompile: src/hz_format
|
||||
Recompile: src/hz_fetcher
|
||||
Recompile: src/hz
|
||||
Recompile: src/hakuzaru
|
||||
Starting otpr-hakuzaru-0.8.0.
|
||||
hz_man starting.
|
||||
Started [hakuzaru]
|
||||
1> hz:chain_nodes([{"groot.testnet.gajumaru.io", 3013}]).
|
||||
ok
|
||||
2> vanillae:status().
|
||||
{error,no_nodes}
|
||||
3> vanillae:ae_nodes([{"192.168.7.7", 3013}]).
|
||||
ok
|
||||
4> vanillae:status().
|
||||
{ok,#{"difficulty" => 59729882,
|
||||
2> hz:status().
|
||||
{ok,#{"difficulty" => 2877405482,
|
||||
"finalized" =>
|
||||
#{"hash" =>
|
||||
"kh_PDSn6Xru5JVdpJfdDCNpfsL8gUZvvjyhTYuzgndoy98G5oLLR",
|
||||
"height" => 277454,"type" => "height"},
|
||||
"genesis_key_block_hash" =>
|
||||
"kh_wUCideEB8aDtUaiHCtKcfywU6oHZW6gnyci8Mw6S1RSTCnCRu",
|
||||
"listening" => true,"network_id" => "ae_uat",
|
||||
"kh_Qdi5MTuuhJm7xzn5JUAbYG12cX3qoLMnXrBxPGzBkMWJ4K8vq",
|
||||
"hashrate" => 864394,"listening" => true,
|
||||
"network_id" => "groot.testnet",
|
||||
"node_revision" =>
|
||||
"3a08153c635c53d92029a617f2e784731ba367c6",
|
||||
"node_version" => "6.7.0",
|
||||
"peer_connections" => #{"inbound" => 25,"outbound" => 10},
|
||||
"peer_count" => 50,
|
||||
"7b3cc1db3bb36053023167b86f7d6f2d5dcbd01d",
|
||||
"node_version" => "0.1.0+203.7b3cc1db3",
|
||||
"peer_connections" => #{"inbound" => 1,"outbound" => 3},
|
||||
"peer_count" => 5,
|
||||
"peer_pubkey" =>
|
||||
"pp_fCBqobeSwhdnrzC8DoSsmWbf2GzDK61CJujmsCEd3RUkmh9Ny",
|
||||
"pending_transactions_count" => 2,
|
||||
"pp_2nQHucGyEt5wkYruNuRkg19cbZuEeyR9BZfvtv49F3AoyNSYMT",
|
||||
"pending_transactions_count" => 0,
|
||||
"protocols" =>
|
||||
[#{"effective_at_height" => 425900,"version" => 5},
|
||||
#{"effective_at_height" => 154300,"version" => 4},
|
||||
#{"effective_at_height" => 82900,"version" => 3},
|
||||
#{"effective_at_height" => 40900,"version" => 2},
|
||||
#{"effective_at_height" => 0,"version" => 1}],
|
||||
[#{"effective_at_height" => 0,"version" => 1}],
|
||||
"solutions" => 0,"sync_progress" => 100.0,
|
||||
"syncing" => false,"top_block_height" => 802644,
|
||||
"syncing" => false,"top_block_height" => 277555,
|
||||
"top_hash" =>
|
||||
"kh_2vuNc8eG77aTmHcQDcievjKufFwR4MSSuZbEMWwW5TqUzSQy71",
|
||||
"top_key_block_hash" =>
|
||||
"kh_28LZSvHZPCGqeWsMsqtSjxQjQHKW1pHzoBex97oMT7U2HcLPgV"}}
|
||||
'''
|
||||
|
||||
Alternatively, here is a start function for an application using Vanillae that initializes vanillae_man with a list of nodes provided by a configuration file:
|
||||
|
||||
```
|
||||
start(normal, _Args) ->
|
||||
ok = application:ensure_started(sasl),
|
||||
{ok, Started} = application:ensure_all_started(cowboy),
|
||||
ok = application:ensure_started(vanillae),
|
||||
Nodes = proplists:get_value(ae_nodes, read_config(), []),
|
||||
ok = vanillae:ae_nodes(Nodes),
|
||||
ok = log(info, "Started: ~p~n", [[vanillae | Started]]),
|
||||
Routes = [{'_', [{"/", count_top, []}]}],
|
||||
Dispatch = cowboy_router:compile(Routes),
|
||||
Env = #{env => #{dispatch => Dispatch}},
|
||||
{ok, _} = cowboy:start_clear(count_listener, [{port, 8080}], Env),
|
||||
count_sup:start_link().
|
||||
"kh_2vuNc8eG77aTmHcQDcievjKufFwR4MSSuZbEMWwW5TqUzSQy71"}}
|
||||
'''
|
||||
|
||||
+3
-3
@@ -3,7 +3,7 @@
|
||||
{included_applications,[]},
|
||||
{applications,[stdlib,kernel]},
|
||||
{description,"Gajumaru interoperation library"},
|
||||
{vsn,"0.6.1"},
|
||||
{modules,[hakuzaru,hz,hz_fetcher,hz_grids,hz_key_master,hz_man,
|
||||
hz_sup]},
|
||||
{vsn,"0.9.2"},
|
||||
{modules,[hakuzaru,hz,hz_aaci,hz_fetcher,hz_format,hz_grids,
|
||||
hz_key_master,hz_man,hz_sophia,hz_sup]},
|
||||
{mod,{hakuzaru,[]}}]}.
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@
|
||||
%%% @end
|
||||
|
||||
-module(hakuzaru).
|
||||
-vsn("0.6.1").
|
||||
-vsn("0.9.2").
|
||||
-author("Craig Everett <ceverett@tsuriai.jp>").
|
||||
-copyright("Craig Everett <ceverett@tsuriai.jp>").
|
||||
-license("GPL-3.0-or-later").
|
||||
|
||||
+423
-946
File diff suppressed because it is too large
Load Diff
+1673
File diff suppressed because it is too large
Load Diff
+56
-8
@@ -1,12 +1,18 @@
|
||||
%%% @private
|
||||
%%% Hakuzaru Request Fetcher
|
||||
%%%
|
||||
%%% This module defines the request workers.
|
||||
%%% Each request to a remote chain node is handled by a worker that is spawned
|
||||
%%% to handle it and terminates on completion.
|
||||
%%% @end
|
||||
|
||||
-module(hz_fetcher).
|
||||
-vsn("0.6.1").
|
||||
-vsn("0.9.2").
|
||||
-author("Craig Everett <ceverett@tsuriai.jp>").
|
||||
-copyright("Craig Everett <ceverett@tsuriai.jp>").
|
||||
-license("MIT").
|
||||
|
||||
-export([connect/4, slowly_connect/4]).
|
||||
|
||||
-include("$zx_include/zx_logger.hrl").
|
||||
-export([connect/4, connect_slowly/4]).
|
||||
|
||||
|
||||
connect(Node = {Host, Port}, Request, From, Timeout) ->
|
||||
@@ -78,7 +84,7 @@ parse(Received, Sock, From, Timer) ->
|
||||
<<"HTTP/1.1 500 Internal Server Error\r\n", Tail/binary>> ->
|
||||
parse2(500, Tail, Sock, From, Timer);
|
||||
_ ->
|
||||
ok = zx_net:disconnect(Sock),
|
||||
ok = disconnect(Sock),
|
||||
ok = erlang:cancel_timer(Timer, [{async, true}]),
|
||||
gen_server:reply(From, {error, {received, Received}})
|
||||
end.
|
||||
@@ -115,7 +121,7 @@ consume2(Length, Received, Sock, From, Timer) ->
|
||||
if
|
||||
Size == Length ->
|
||||
ok = erlang:cancel_timer(Timer, [{async, true}]),
|
||||
ok = zx_net:disconnect(Sock),
|
||||
ok = disconnect(Sock),
|
||||
Result = zj:decode(Received),
|
||||
gen_server:reply(From, Result);
|
||||
Size < Length ->
|
||||
@@ -208,7 +214,7 @@ read_hval(_, Received, _, _, _) ->
|
||||
{error, headers}.
|
||||
|
||||
|
||||
slowly_connect(Node, {get, Path}, From, Timeout) ->
|
||||
connect_slowly(Node, {get, Path}, From, Timeout) ->
|
||||
HttpOptions = [{connect_timeout, 3000}, {timeout, Timeout}],
|
||||
URL = lists:flatten(url(Node, Path)),
|
||||
Request = {URL, []},
|
||||
@@ -219,7 +225,7 @@ slowly_connect(Node, {get, Path}, From, Timeout) ->
|
||||
BAD -> {error, BAD}
|
||||
end,
|
||||
gen_server:reply(From, Result);
|
||||
slowly_connect(Node, {post, Path, Payload}, From, Timeout) ->
|
||||
connect_slowly(Node, {post, Path, Payload}, From, Timeout) ->
|
||||
HttpOptions = [{connect_timeout, 3000}, {timeout, Timeout}],
|
||||
URL = lists:flatten(url(Node, Path)),
|
||||
Request = {URL, [], "application/json", Payload},
|
||||
@@ -236,3 +242,45 @@ url({Node, Port}, Path) when is_list(Node) ->
|
||||
["https://", Node, ":", integer_to_list(Port), Path];
|
||||
url({Node, Port}, Path) when is_tuple(Node) ->
|
||||
["https://", inet:ntoa(Node), ":", integer_to_list(Port), Path].
|
||||
|
||||
|
||||
disconnect(Socket) ->
|
||||
case peername(Socket) of
|
||||
{ok, {Addr, Port}} ->
|
||||
Host = inet:ntoa(Addr),
|
||||
disconnect(Socket, Host, Port);
|
||||
{error, Reason} ->
|
||||
log(warning, "Disconnect failed with: ~w", [Reason])
|
||||
end.
|
||||
|
||||
disconnect(Socket, Host, Port) ->
|
||||
case gen_tcp:shutdown(Socket, read_write) of
|
||||
ok ->
|
||||
ok;
|
||||
{error, enotconn} ->
|
||||
receive
|
||||
{tcp_closed, Socket} -> ok
|
||||
after 0 -> ok
|
||||
end;
|
||||
{error, E} ->
|
||||
ok = log(warning, "~ts:~w disconnect failed with: ~w", [Host, Port, E]),
|
||||
receive
|
||||
{tcp_closed, Socket} -> ok
|
||||
after 0 -> ok
|
||||
end
|
||||
end.
|
||||
|
||||
peername(Socket) ->
|
||||
case inet:peername(Socket) of
|
||||
{ok, {{0, 0, 0, 0, 0, 65535, X, Y}, Port}} ->
|
||||
<<A:8, B:8, C:8, D:8>> = <<X:16, Y:16>>,
|
||||
{ok, {{A, B, C, D}, Port}};
|
||||
Other ->
|
||||
Other
|
||||
end.
|
||||
|
||||
|
||||
log(Level, Format, Args) ->
|
||||
Raw = io_lib:format("~w ~w: " ++ Format, [?MODULE, self() | Args]),
|
||||
Entry = unicode:characters_to_list(Raw),
|
||||
logger:log(Level, Entry).
|
||||
|
||||
@@ -0,0 +1,745 @@
|
||||
%%% @doc
|
||||
%%% Formatting and reading functions for Gaju and Puck quantities
|
||||
%%%
|
||||
%%% The numbers involved in dealing with blockchain amounts are enormous
|
||||
%%% by comparison to legacy forms of currency. It isn't so much that
|
||||
%%% thousands of Gajus is hard to reason about, but rather that quadrillions
|
||||
%%% of Pucks is quite hard to even lock on to visually.
|
||||
%%%
|
||||
%%% A normal commas and underscores method of decimal formatting is provided, as
|
||||
%%% `us' formatting along with two additional approaches:
|
||||
%%% - Japanese traditional myriad structure (`jp' style)
|
||||
%%% - An internationalized variant inspired by the Japanese technique over periods
|
||||
%%% (`metric' for SI prefixes, and `legacy' for Anglicized prefixes)
|
||||
%%%
|
||||
%%% These are all accessible via the `amount/N' functions.
|
||||
%%%
|
||||
%%% The `read/1' function can accept any of the output variants as a string and
|
||||
%%% will return the number of pucks indicated by the provided string, allowing for
|
||||
%%% simple copy/paste functionality as well as direct input using any of the
|
||||
%%% supported notations.
|
||||
%%% @end
|
||||
|
||||
-module(hz_format).
|
||||
-vsn("0.9.2").
|
||||
-author("Craig Everett <ceverett@tsuriai.jp>").
|
||||
-copyright("Craig Everett <ceverett@tsuriai.jp>").
|
||||
-license("GPL-3.0-or-later").
|
||||
|
||||
-export([amount/1, amount/2, amount/3,
|
||||
approx_amount/2, approx_amount/3,
|
||||
read/1,
|
||||
one/1, mark/1,
|
||||
price_to_string/1, string_to_price/1]).
|
||||
|
||||
-spec amount(Pucks) -> Formatted
|
||||
when Pucks :: integer(),
|
||||
Formatted :: string().
|
||||
%% @doc
|
||||
%% A convenience formatting function.
|
||||
%% ```
|
||||
%% hz_format:amount(1) ->
|
||||
%% 木0.000,000,000,000,000,001
|
||||
%%
|
||||
%% hz_format:amount(5000) ->
|
||||
%% 木0.000,000,000,000,005
|
||||
%%
|
||||
%% hz_format:amount(5000000000000000000) ->
|
||||
%% 木5
|
||||
%%
|
||||
%% hz_format:amount(500000123000000000000000) ->
|
||||
%% 木500,000.123
|
||||
%% '''
|
||||
%% @equiv amount(us, Pucks)
|
||||
|
||||
amount(Pucks) ->
|
||||
amount(us, Pucks).
|
||||
|
||||
|
||||
-spec amount(Style, Pucks) -> Formatted
|
||||
when Style :: us | jp | metric | legacy | {Separator, Span},
|
||||
Separator :: $, | $_,
|
||||
Span :: 3 | 4,
|
||||
Pucks :: integer(),
|
||||
Formatted :: string().
|
||||
%% @doc
|
||||
%% A money formatting function.
|
||||
%% ```
|
||||
%% hz_format:amount(us, 100500040123000000000000000) ->
|
||||
%% 木100,500,040.123
|
||||
%%
|
||||
%% hz_format:amount(jp, 100500040123000000000000000) ->
|
||||
%% 1億50万40木 12京3000兆本
|
||||
%%
|
||||
%% hz_format:amount(metric, 100500040123000000000000000) ->
|
||||
%% 木100m 500k 40 G 123p P
|
||||
%%
|
||||
%% hz_format:amount(legacy, 100500040123000000000000000) ->
|
||||
%% 木100m 500k 40 G 123q P
|
||||
%%
|
||||
%% hz_format:amount({$_, 3}, 100500040123000000000000000) ->
|
||||
%% 木100_500_040.123
|
||||
%%
|
||||
%% hz_format:amount({$_, 4}, 100500040123000000000000000) ->
|
||||
%% 木1_0050_0040.123
|
||||
%% '''
|
||||
%% @equiv amount(gaju, Style, Pucks)
|
||||
|
||||
amount(Style, Pucks) ->
|
||||
amount(gaju, Style, Pucks).
|
||||
|
||||
|
||||
-spec amount(Unit, Style, Pucks) -> Formatted
|
||||
when Unit :: gaju | puck,
|
||||
Style :: us | jp | metric | legacy | {Separator, Span},
|
||||
Separator :: $, | $_,
|
||||
Span :: 3 | 4,
|
||||
Pucks :: integer(),
|
||||
Formatted :: string().
|
||||
%% @doc
|
||||
%% A simplified format function covering the most common formats desired.
|
||||
%% ```
|
||||
%% hz_format:amount(gaju, us, 100500040123000004500000000) ->
|
||||
%% 木100,500,040.123,000,004,5
|
||||
%%
|
||||
%% hz_format:amount(puck, us, 100500040123000004500000000) ->
|
||||
%% 本100,500,040,123,000,004,500,000,000
|
||||
%%
|
||||
%% hz_format:amount(gaju, jp, 100500040123000004500000000) ->
|
||||
%% 1億50万40木 12京3000兆45億本
|
||||
%%
|
||||
%% hz_format:amount(puck, jp, 100500040123000004500000000) ->
|
||||
%% 100秭5000垓4012京3000兆45億本
|
||||
%%
|
||||
%% hz_format:amount(gaju, metric, 100500040123000004500000000) ->
|
||||
%% 木100m 500k 40 G 123p 4g 500m P
|
||||
%%
|
||||
%% hz_format:amount(puck, metric, 100500040123000004500000000) ->
|
||||
%% 本100y 500z 40e 123p 4g 500m P
|
||||
%%
|
||||
%% hz_format:amount(gaju, legacy, 100500040123000004500000000) ->
|
||||
%% 木100m 500k 40 G 123q 4b 500m P
|
||||
%%
|
||||
%% hz_format:amount(puck, legacy, 100500040123000004500000000) ->
|
||||
%% 本100y 500z 40e 123q 4b 500m P
|
||||
%% '''
|
||||
|
||||
amount(gaju, us, Pucks) ->
|
||||
western($,, $., 3, all, Pucks);
|
||||
amount(puck, us, Pucks) ->
|
||||
western($,, 3, Pucks);
|
||||
amount(Unit, jp, Pucks) ->
|
||||
jp(Unit, all, Pucks);
|
||||
amount(Unit, metric, Pucks) ->
|
||||
bestern(Unit, ranks(metric), all, Pucks);
|
||||
amount(Unit, legacy, Pucks) ->
|
||||
bestern(Unit, ranks(heresy), all, Pucks);
|
||||
amount(gaju, {Separator, Span}, Pucks) ->
|
||||
western(Separator, $., Span, all, Pucks);
|
||||
amount(puck, {Separator, Span}, Pucks) ->
|
||||
western(Separator, Span, Pucks).
|
||||
|
||||
|
||||
|
||||
-spec approx_amount(Precision, Pucks) -> Serialized
|
||||
when Precision :: all | 0..18,
|
||||
Pucks :: integer(),
|
||||
Serialized :: string().
|
||||
%% A formatter for decimal notation which permits a precision
|
||||
%% value to be applied to the puck side of the format.
|
||||
%% ```
|
||||
%% hz_format:approx_amount(3, 100500040123000004500000001) ->
|
||||
%% 木100,500,040.123
|
||||
%%
|
||||
%% hz_format:approx_amount(13, 100500040123000004500000001) ->
|
||||
%% 木100,500,040.123,000,004,5...
|
||||
%%
|
||||
%% hz_format:approx_amount(all, 100500040123000004500000001) ->
|
||||
%% 木100,500,040.123,000,004,500,000,001
|
||||
%% '''
|
||||
%% @equiv approx_amount(us, Precision, Pucks)
|
||||
|
||||
approx_amount(Precision, Pucks) ->
|
||||
approx_amount(us, Precision, Pucks).
|
||||
|
||||
|
||||
-spec approx_amount(Style, Precision, Pucks) -> Serialized
|
||||
when Style :: us | {Separator, Span},
|
||||
Precision :: all | 0..18,
|
||||
Separator :: $, | $_,
|
||||
Span :: 3 | 4,
|
||||
Pucks :: integer(),
|
||||
Serialized :: string().
|
||||
%% @doc
|
||||
%% A formatter for decimal notation which permits a precision
|
||||
%% value to be applied to the puck side of the format.
|
||||
%% ```
|
||||
%% hz_format:approx_amount({$_, 3}, 3, 100500040123000004500000001) ->
|
||||
%% 木100_500_040.123...
|
||||
%%
|
||||
%% hz_format:approx_amount({$_, 4}, 12, 100500040123000004500000001) ->
|
||||
%% 木1_0050_0040.1230_0000_45...
|
||||
%% '''
|
||||
|
||||
approx_amount(us, Precision, Pucks) ->
|
||||
western($,, $., 3, Precision, Pucks);
|
||||
approx_amount({Separator, Span}, Precision, Pucks) ->
|
||||
western(Separator, $., Span, Precision, Pucks).
|
||||
|
||||
|
||||
western(Separator, Span, Pucks) when Pucks >= 0 ->
|
||||
western2(Separator, Span, Pucks);
|
||||
western(Separator, Span, Pucks) when Pucks < 0 ->
|
||||
[$- | western2(Separator, Span, Pucks * -1)].
|
||||
|
||||
western2(Separator, Span, Pucks) ->
|
||||
P = lists:reverse(integer_to_list(Pucks)),
|
||||
[mark(puck) | separate(Separator, Span, P)].
|
||||
|
||||
|
||||
western(Separator, Break, Span, Precision, Pucks) when Pucks >= 0 ->
|
||||
western2(Separator, Break, Span, Precision, Pucks);
|
||||
western(Separator, Break, Span, Precision, Pucks) when Pucks < 0 ->
|
||||
[$- | western2(Separator, Break, Span, Precision, Pucks * -1)].
|
||||
|
||||
|
||||
western2(Separator, _, Span, 0, Pucks) ->
|
||||
G = lists:reverse(integer_to_list(Pucks div one(gaju))),
|
||||
[mark(gaju) | separate(Separator, Span, G)];
|
||||
western2(Separator, Break, Span, Precision, Pucks) ->
|
||||
SP = integer_to_list(Pucks),
|
||||
Length = length(SP),
|
||||
Over18 = Length > 18,
|
||||
NoPucks = (Pucks rem one(gaju)) =:= 0,
|
||||
case {Over18, NoPucks} of
|
||||
{true, true} ->
|
||||
Gs = lists:reverse(lists:sublist(SP, Length - 18)),
|
||||
[mark(gaju) | separate(Separator, Span, Gs)];
|
||||
{true, false} ->
|
||||
{PChars, GChars} = lists:split(18, lists:reverse(SP)),
|
||||
H = [mark(gaju) | separate(Separator, Span, GChars)],
|
||||
{P, E} = decimal_pucks(Precision, lists:reverse(PChars)),
|
||||
T = lists:reverse(separate(Separator, Span, P)),
|
||||
lists:flatten([H, Break, T, E]);
|
||||
{false, true} ->
|
||||
[mark(gaju), $0];
|
||||
{false, false} ->
|
||||
PChars = lists:flatten(string:pad(SP, 18, leading, $0)),
|
||||
{P, E} = decimal_pucks(Precision, PChars),
|
||||
T = lists:reverse(separate(Separator, Span, P)),
|
||||
lists:flatten([mark(gaju), $0, Break, T, E])
|
||||
end.
|
||||
|
||||
decimal_pucks(all, PChars) ->
|
||||
RTrailing = lists:reverse(PChars),
|
||||
{lists:reverse(lists:dropwhile(fun(C) -> C =:= $0 end, RTrailing)), ""};
|
||||
decimal_pucks(Precision, PChars) ->
|
||||
{Significant, Rest} = lists:split(min(Precision, 18), PChars),
|
||||
RTrailing = lists:reverse(Significant),
|
||||
Trailing = lists:reverse(lists:dropwhile(fun(C) -> C =:= $0 end, RTrailing)),
|
||||
case lists:all(fun(C) -> C =:= $0 end, Rest) of
|
||||
true -> {Trailing, ""};
|
||||
false -> {Trailing, "..."}
|
||||
end.
|
||||
|
||||
separate(_, _, "") ->
|
||||
"";
|
||||
separate(S, P, G) ->
|
||||
separate(S, P, 1, G, []).
|
||||
|
||||
separate(_, _, _, [H], A) ->
|
||||
[H | A];
|
||||
separate(S, P, P, [H | T], A) ->
|
||||
separate(S, P, 1, T, [S, H | A]);
|
||||
separate(S, P, N, [H | T], A) ->
|
||||
separate(S, P, N + 1, T, [H | A]).
|
||||
|
||||
|
||||
bestern(gaju, Ranks, Precision, Pucks) when Pucks >= 0 ->
|
||||
[mark(gaju), bestern2(gaju, Ranks, 3, Precision, Pucks)];
|
||||
bestern(gaju, Ranks, Precision, Pucks) when Pucks < 0 ->
|
||||
[$-, mark(gaju), bestern2(gaju, Ranks, 3, Precision, Pucks * -1)];
|
||||
bestern(puck, Ranks, Precision, Pucks) when Pucks >= 0 ->
|
||||
[mark(puck), bestern2(puck, Ranks, 3, Precision, Pucks)];
|
||||
bestern(puck, Ranks, Precision, Pucks) when Pucks < 0 ->
|
||||
[$-, mark(puck), bestern2(puck, Ranks, 3, Precision, Pucks * -1)].
|
||||
|
||||
jp(Unit, Precision, Pucks) when Pucks >= 0 ->
|
||||
bestern2(Unit, ranks(jp), 4, Precision, Pucks);
|
||||
jp(Unit, Precision, Pucks) when Pucks < 0 ->
|
||||
[$-, bestern2(Unit, ranks(jp), 4, Precision, Pucks * -1)].
|
||||
|
||||
bestern2(gaju, Ranks, Span, 0, Pucks) ->
|
||||
G = lists:reverse(integer_to_list(Pucks div one(gaju))),
|
||||
case Span of
|
||||
3 -> period("G", Ranks, G);
|
||||
4 -> myriad(mark(gaju), Ranks, G)
|
||||
end;
|
||||
bestern2(gaju, Ranks, Span, all, Pucks) ->
|
||||
P = lists:flatten(string:pad(integer_to_list(Pucks rem one(gaju)), 18, leading, $0)),
|
||||
Zilch = lists:all(fun(C) -> C =:= $0 end, P),
|
||||
{H, T} =
|
||||
case {Span, Zilch} of
|
||||
{3, false} -> {bestern2(gaju, Ranks, 3, 0, Pucks), period("P", Ranks, lists:reverse(P))};
|
||||
{4, false} -> {jp(gaju, 0, Pucks), myriad(mark(puck), Ranks, lists:reverse(P))};
|
||||
{3, true} -> {bestern2(gaju, Ranks, 3, 0, Pucks), ""};
|
||||
{4, true} -> {jp(gaju, 0, Pucks), ""}
|
||||
end,
|
||||
lists:flatten([H, " ", T]);
|
||||
bestern2(gaju, Ranks, Span, Precision, Pucks) ->
|
||||
P = lists:flatten(string:pad(integer_to_list(Pucks rem one(gaju)), 18, leading, $0)),
|
||||
H =
|
||||
case Span of
|
||||
3 -> bestern2(gaju, Ranks, 3, 0, Pucks);
|
||||
4 -> jp(gaju, 0, Pucks)
|
||||
end,
|
||||
Digits = min(Precision, 18),
|
||||
T =
|
||||
case length(P) < Digits of
|
||||
false ->
|
||||
ReverseP = lists:reverse(lists:sublist(P, Digits)),
|
||||
PuckingString = lists:flatten(string:pad(ReverseP, 18, leading, $0)),
|
||||
case lists:all(fun(C) -> C =:= $0 end, PuckingString) of
|
||||
false ->
|
||||
case Span of
|
||||
3 -> period("P", Ranks, PuckingString);
|
||||
4 -> myriad(mark(puck), Ranks, PuckingString)
|
||||
end;
|
||||
true ->
|
||||
""
|
||||
end;
|
||||
true ->
|
||||
[]
|
||||
end,
|
||||
lists:flatten([H, " ", T]);
|
||||
bestern2(puck, Ranks, Span, all, Pucks) ->
|
||||
P = lists:reverse(integer_to_list(Pucks)),
|
||||
case lists:all(fun(C) -> C =:= $0 end, P) of
|
||||
false ->
|
||||
case Span of
|
||||
3 -> period("P", Ranks, P);
|
||||
4 -> myriad(mark(puck), Ranks, P)
|
||||
end;
|
||||
true ->
|
||||
case Span of
|
||||
3 -> [$0, " P"];
|
||||
4 -> [$0, mark(puck)]
|
||||
end
|
||||
end;
|
||||
bestern2(puck, Ranks, Span, Precision, Pucks) ->
|
||||
Digits = min(Precision, 18),
|
||||
P = lists:flatten(string:pad(integer_to_list(Pucks), 18, leading, $0)),
|
||||
case length(P) < Digits of
|
||||
true ->
|
||||
case Span of
|
||||
3 -> [$0, " P"];
|
||||
4 -> [$0, mark(puck)]
|
||||
end;
|
||||
false ->
|
||||
PucksToGive = lists:sublist(P, Digits),
|
||||
PuckingString = lists:flatten(string:pad(lists:reverse(PucksToGive), 18, leading, $0)),
|
||||
case lists:all(fun(C) -> C =:= $0 end, PuckingString) of
|
||||
false ->
|
||||
case Span of
|
||||
3 -> period("P", Ranks, PuckingString);
|
||||
4 -> myriad(mark(puck), Ranks, PuckingString)
|
||||
end;
|
||||
true ->
|
||||
case Span of
|
||||
3 -> [$0, " P"];
|
||||
4 -> [$0, mark(puck)]
|
||||
end
|
||||
end
|
||||
end.
|
||||
|
||||
|
||||
period(Symbol, Ranks, [$0, $0, $0 | PT]) ->
|
||||
rank3(Ranks, PT, [Symbol]);
|
||||
period(Symbol, Ranks, [P3, $0, $0 | PT]) ->
|
||||
rank3(Ranks, PT, [P3, 32, Symbol]);
|
||||
period(Symbol, Ranks, [P3, P2, $0 | PT]) ->
|
||||
rank3(Ranks, PT, [P2, P3, 32, Symbol]);
|
||||
period(Symbol, Ranks, [P3, P2, P1 | PT]) ->
|
||||
rank3(Ranks, PT, [P1, P2, P3, 32, Symbol]);
|
||||
period(Symbol, _, [P3]) ->
|
||||
[P3, 32, Symbol];
|
||||
period(Symbol, _, [P3, P2]) ->
|
||||
[P2, P3, 32, Symbol].
|
||||
|
||||
rank3([_ | RT], [$0, $0, $0 | PT], A) ->
|
||||
rank3(RT, PT, A);
|
||||
rank3([RH | RT], [P3, $0, $0 | PT], A) ->
|
||||
rank3(RT, PT, [P3, RH | A]);
|
||||
rank3([RH | RT], [P3, P2, $0 | PT], A) ->
|
||||
rank3(RT, PT, [P2, P3, RH | A]);
|
||||
rank3([RH | RT], [P3, P2, P1 | PT], A) ->
|
||||
rank3(RT, PT, [P1, P2, P3, RH | A]);
|
||||
rank3(_, [$0, $0, $0], A) ->
|
||||
A;
|
||||
rank3(_, [$0, $0], A) ->
|
||||
A;
|
||||
rank3(_, [$0], A) ->
|
||||
A;
|
||||
rank3(_, [], A) ->
|
||||
A;
|
||||
rank3([RH | _], [P3, $0, $0], A) ->
|
||||
[P3, RH | A];
|
||||
rank3([RH | _], [P3, $0], A) ->
|
||||
[P3, RH | A];
|
||||
rank3([RH | _], [P3], A) ->
|
||||
[P3, RH | A];
|
||||
rank3([RH | _], [P3, P2, $0], A) ->
|
||||
[P2, P3, RH | A];
|
||||
rank3([RH | _], [P3, P2], A) ->
|
||||
[P2, P3, RH | A];
|
||||
rank3([RH | _], [P3, P2, P1], A) ->
|
||||
[P1, P2, P3, RH | A].
|
||||
|
||||
|
||||
myriad(Symbol, Ranks, [$0, $0, $0, $0 | PT]) ->
|
||||
rank4(Ranks, PT, [Symbol]);
|
||||
myriad(Symbol, Ranks, [P4, $0, $0, $0 | PT]) ->
|
||||
rank4(Ranks, PT, [P4, Symbol]);
|
||||
myriad(Symbol, Ranks, [P4, P3, $0, $0 | PT]) ->
|
||||
rank4(Ranks, PT, [P3, P4, Symbol]);
|
||||
myriad(Symbol, Ranks, [P4, P3, P2, $0 | PT]) ->
|
||||
rank4(Ranks, PT, [P2, P3, P4, Symbol]);
|
||||
myriad(Symbol, Ranks, [P4, P3, P2, P1 | PT]) ->
|
||||
rank4(Ranks, PT, [P1, P2, P3, P4, Symbol]);
|
||||
myriad(Symbol, _, [P4]) ->
|
||||
[P4, Symbol];
|
||||
myriad(Symbol, _, [P4, P3]) ->
|
||||
[P3, P4, Symbol];
|
||||
myriad(Symbol, _, [P4, P3, P2]) ->
|
||||
[P2, P3, P4, Symbol].
|
||||
|
||||
rank4([_ | RT], [$0, $0, $0, $0 | PT], A) ->
|
||||
rank4(RT, PT, A);
|
||||
rank4([RH | RT], [P4, $0, $0, $0 | PT], A) ->
|
||||
rank4(RT, PT, [P4, RH | A]);
|
||||
rank4([RH | RT], [P4, P3, $0, $0 | PT], A) ->
|
||||
rank4(RT, PT, [P3, P4, RH | A]);
|
||||
rank4([RH | RT], [P4, P3, P2, $0 | PT], A) ->
|
||||
rank4(RT, PT, [P2, P3, P4, RH | A]);
|
||||
rank4([RH | RT], [P4, P3, P2, P1 | PT], A) ->
|
||||
rank4(RT, PT, [P1, P2, P3, P4, RH | A]);
|
||||
rank4(_, [$0, $0, $0, $0], A) ->
|
||||
A;
|
||||
rank4(_, [$0, $0, $0], A) ->
|
||||
A;
|
||||
rank4(_, [$0, $0], A) ->
|
||||
A;
|
||||
rank4(_, [$0], A) ->
|
||||
A;
|
||||
rank4(_, [], A) ->
|
||||
A;
|
||||
rank4([RH | _], [P4, $0, $0, $0], A) ->
|
||||
[P4, RH | A];
|
||||
rank4([RH | _], [P4, $0, $0], A) ->
|
||||
[P4, RH | A];
|
||||
rank4([RH | _], [P4, $0], A) ->
|
||||
[P4, RH | A];
|
||||
rank4([RH | _], [P4], A) ->
|
||||
[P4, RH | A];
|
||||
rank4([RH | _], [P4, P3, $0, $0], A) ->
|
||||
[P3, P4, RH | A];
|
||||
rank4([RH | _], [P4, P3, $0], A) ->
|
||||
[P3, P4, RH | A];
|
||||
rank4([RH | _], [P4, P3], A) ->
|
||||
[P3, P4, RH | A];
|
||||
rank4([RH | _], [P4, P3, P2, $0], A) ->
|
||||
[P2, P3, P4, RH | A];
|
||||
rank4([RH | _], [P4, P3, P2], A) ->
|
||||
[P2, P3, P4, RH | A];
|
||||
rank4([RH | _], [P4, P3, P2, P1], A) ->
|
||||
[P1, P2, P3, P4, RH | A].
|
||||
|
||||
ranks(jp) ->
|
||||
"万億兆京垓秭穣溝澗正載極";
|
||||
ranks(metric) ->
|
||||
["k ", "m ", "g ", "t ", "p ", "e ", "z ", "y ", "r ", "Q "];
|
||||
ranks(heresy) ->
|
||||
["k ", "m ", "b ", "t ", "q ", "e ", "z ", "y ", "r ", "Q "].
|
||||
|
||||
|
||||
-spec mark(Unit) -> Mark
|
||||
when Unit :: gaju | puck,
|
||||
Mark :: $木 | $本.
|
||||
%% @doc
|
||||
%% Retrieve the unicode codepoint for the `gaju' mark (木) or the `puck' mark (本).
|
||||
|
||||
mark(gaju) -> $木;
|
||||
mark(puck) -> $本.
|
||||
|
||||
|
||||
-spec one(Unit) -> Pucks
|
||||
when Unit :: gaju | puck,
|
||||
Pucks :: 1_000_000_000_000_000_000 | 1.
|
||||
%% @doc
|
||||
%% Quickly resolve the number of pucks in a given unit.
|
||||
%%
|
||||
%% The number of pucks in a gaju is so large that it can be a little bit annoying
|
||||
%% to remember the exact amount. This is a helper to simplify this when writing
|
||||
%% an app against the hakuzaru library when dealing in either unit.
|
||||
|
||||
one(gaju) -> 1_000_000_000_000_000_000;
|
||||
one(puck) -> 1.
|
||||
|
||||
|
||||
-spec read(Format) -> Result
|
||||
when Format :: string(),
|
||||
Result :: {ok, Pucks} | error,
|
||||
Pucks :: integer().
|
||||
%% @doc
|
||||
%% Convert any valid string formatted representation and output a value in pucks.
|
||||
%% NOTE: This function does not accept approximated values.
|
||||
%% ```
|
||||
%% 1> hz_format:read("木100,500,040.123,000,004,5").
|
||||
%% {ok,100500040123000004500000000}
|
||||
%% 2> hz_format:read("本100,500,040,123,000,004,500,000,000").
|
||||
%% {ok,100500040123000004500000000}
|
||||
%% 3> hz_format:read("1億50万40木 12京3000兆45億本").
|
||||
%% {ok,100500040123000004500000000}
|
||||
%% 4> hz_format:read("100秭5000垓4012京3000兆45億本").
|
||||
%% {ok,100500040123000004500000000}
|
||||
%% 5> hz_format:read("木100m 500k 40 G 123p 4g 500m P").
|
||||
%% {ok,100500040123000004500000000}
|
||||
%% 6> hz_format:read("本100y 500z 40e 123p 4g 500m P").
|
||||
%% {ok,100500040123000004500000000}
|
||||
%% 7> hz_format:read("木100m 500k 40 G 123q 4b 500m P").
|
||||
%% {ok,100500040123000004500000000}
|
||||
%% 8> hz_format:read("本100y 500z 40e 123q 4b 500m P").
|
||||
%% {ok,100500040123000004500000000}
|
||||
%% '''
|
||||
|
||||
read([$木 | Rest]) ->
|
||||
read_w_gajus(Rest, []);
|
||||
read([$本 | Rest]) ->
|
||||
read_w_pucks(Rest, []);
|
||||
read([C | Rest])
|
||||
when C =:= $- orelse
|
||||
C =:= $− orelse
|
||||
C =:= $- ->
|
||||
case read(Rest) of
|
||||
{ok, Pucks} -> {ok, Pucks * -1};
|
||||
Error -> Error
|
||||
end;
|
||||
read([C | Rest])
|
||||
when C =:= 32 orelse % ASCII space
|
||||
C =:= 12288 orelse % full-width space
|
||||
C =:= $\t orelse
|
||||
C =:= $\r orelse
|
||||
C =:= $\n ->
|
||||
read(Rest);
|
||||
read([C | Rest]) when $0 =< C andalso C =< $9 ->
|
||||
read(Rest, [C], []);
|
||||
read([C | Rest]) when $0 =< C andalso C =< $9 ->
|
||||
NumC = C - $0 + $0,
|
||||
read(Rest, [NumC], []);
|
||||
read(String) when is_binary(String) ->
|
||||
read(binary_to_list(String));
|
||||
read(_) ->
|
||||
error.
|
||||
|
||||
read_w_gajus([C | Rest], A) when $0 =< C andalso C =< $9 ->
|
||||
read_w_gajus(Rest, [C | A]);
|
||||
read_w_gajus([C | Rest], A) when $0 =< C andalso C =< $9 ->
|
||||
NumC = C - $0 + $0,
|
||||
read_w_gajus(Rest, [NumC | A]);
|
||||
read_w_gajus([$, | Rest], A) ->
|
||||
read_w_gajus(Rest, A);
|
||||
read_w_gajus([$_ | Rest], A) ->
|
||||
read_w_gajus(Rest, A);
|
||||
read_w_gajus([$. | Rest], A) ->
|
||||
case read_w_pucks(Rest, []) of
|
||||
{ok, P} ->
|
||||
G = list_to_integer(lists:reverse(A)) * one(gaju),
|
||||
{ok, G + P};
|
||||
Error ->
|
||||
Error
|
||||
end;
|
||||
read_w_gajus([], A) ->
|
||||
G = list_to_integer(lists:reverse(A)) * one(gaju),
|
||||
{ok, G};
|
||||
read_w_gajus([C, 32 | Rest], A) ->
|
||||
read(Rest, [], [{C, A}]);
|
||||
read_w_gajus([32, $G, 32 | Rest], A) ->
|
||||
read(Rest, [], [{$G, A}], []);
|
||||
read_w_gajus([32, $G], A) ->
|
||||
calc([{$G, A}], []);
|
||||
read_w_gajus([32, $P], A) ->
|
||||
calc([], [{$P, A}]);
|
||||
read_w_gajus(_, _) ->
|
||||
error.
|
||||
|
||||
read_w_pucks([C | Rest], A) when $0 =< C andalso C =< $9 ->
|
||||
read_w_pucks(Rest, [C | A]);
|
||||
read_w_pucks([C | Rest], A) when $0 =< C andalso C =< $9 ->
|
||||
NumC = C - $0 + $0,
|
||||
read_w_pucks(Rest, [NumC | A]);
|
||||
read_w_pucks([$, | Rest], A) ->
|
||||
read_w_pucks(Rest, A);
|
||||
read_w_pucks([$_ | Rest], A) ->
|
||||
read_w_pucks(Rest, A);
|
||||
read_w_pucks([C, 32 | Rest], A) ->
|
||||
read(Rest, [], [], [{C, A}]);
|
||||
read_w_pucks([32, $P], A) ->
|
||||
calc([], [{$P, A}]);
|
||||
read_w_pucks([], A) ->
|
||||
Padded = lists:flatten(string:pad(lists:reverse(A), 18, trailing, $0)),
|
||||
{ok, list_to_integer(Padded)}.
|
||||
|
||||
|
||||
read([C | Rest], A, G) when $0 =< C andalso C =< $9 ->
|
||||
read(Rest, [C | A], G);
|
||||
read([C | Rest], A, G) when $0 =< C andalso C =< $9 ->
|
||||
NumC = C - $0 + $0,
|
||||
read(Rest, [NumC | A], G);
|
||||
read([$木], A, G) ->
|
||||
calc([{$G, A} | G], []);
|
||||
read([$G], A, G) ->
|
||||
calc([{$G, A} | G], []);
|
||||
read([32, $G], A, G) ->
|
||||
calc([{$G, A} | G], []);
|
||||
read([32, $G, 32 | Rest], A, G) ->
|
||||
read(Rest, [], [{$G, A} | G], []);
|
||||
read([$木, 32 | Rest], A, G) ->
|
||||
read(Rest, [], [{$G, A} | G], []);
|
||||
read([$G, 32 | Rest], A, G) ->
|
||||
read(Rest, [], [{$G, A} | G], []);
|
||||
read([$本], A, P) ->
|
||||
calc([], [{$P, A} | P]);
|
||||
read([$P], A, P) ->
|
||||
calc([], [{$P, A} | P]);
|
||||
read([32, $P], A, P) ->
|
||||
calc([], [{$P, A} | P]);
|
||||
read([C, 32 | Rest], A, G) ->
|
||||
read(Rest, [], [{C, A} | G]);
|
||||
read([$, | Rest], A, []) ->
|
||||
read_w_gajus(Rest, A);
|
||||
read([$_ | Rest], A, []) ->
|
||||
read_w_gajus(Rest, A);
|
||||
read([$. | Rest], A, []) ->
|
||||
case read_w_pucks(Rest, []) of
|
||||
{ok, P} ->
|
||||
G = list_to_integer(lists:reverse(A)) * one(gaju),
|
||||
{ok, G + P};
|
||||
Error ->
|
||||
Error
|
||||
end;
|
||||
read([C | Rest], A, G) ->
|
||||
read(Rest, [], [{C, A} | G]);
|
||||
read([], A, []) ->
|
||||
read_w_gajus([], A);
|
||||
read(_, _, _) ->
|
||||
error.
|
||||
|
||||
|
||||
read([C | Rest], A, G, P) when $0 =< C andalso C =< $9 ->
|
||||
read(Rest, [C | A], G, P);
|
||||
read([C | Rest], A, G, P) when $0 =< C andalso C =< $9 ->
|
||||
NumC = C - $0 + $0,
|
||||
read(Rest, [NumC | A], G, P);
|
||||
read([$本], A, G, P) ->
|
||||
calc(G, [{$P, A} | P]);
|
||||
read([$P], A, G, P) ->
|
||||
calc(G, [{$P, A} | P]);
|
||||
read([32, $P], A, G, P) ->
|
||||
calc(G, [{$P, A} | P]);
|
||||
read([C, 32 | Rest], A, G, P) ->
|
||||
read(Rest, [], G, [{C, A} | P]);
|
||||
read([C | Rest], A, G, P) ->
|
||||
read(Rest, [], G, [{C, A} | P]);
|
||||
read(_, _, _, _) ->
|
||||
error.
|
||||
|
||||
calc(G, P) ->
|
||||
case calc(gaju, G, 0) of
|
||||
{ok, Gajus} ->
|
||||
case calc(puck, P, 0) of
|
||||
{ok, Pucks} -> {ok, Gajus + Pucks};
|
||||
error -> error
|
||||
end;
|
||||
error ->
|
||||
error
|
||||
end.
|
||||
|
||||
calc(U, [{_, []} | S], A) ->
|
||||
calc(U, S, A);
|
||||
calc(U, [{M, Cs} | S], A) ->
|
||||
case magnitude(M) of
|
||||
{ok, J} ->
|
||||
N = list_to_integer(lists:reverse(Cs)) * J * one(U),
|
||||
calc(U, S, A + N);
|
||||
Error ->
|
||||
Error
|
||||
end;
|
||||
calc(_, [], A) ->
|
||||
{ok, A}.
|
||||
|
||||
|
||||
magnitude($G) ->
|
||||
{ok, 1};
|
||||
magnitude($P) ->
|
||||
{ok, 1};
|
||||
magnitude(Mark) ->
|
||||
case rank(Mark, ranks(jp), 1_0000, 1) of
|
||||
{ok, J} ->
|
||||
{ok, J};
|
||||
error ->
|
||||
case rank([Mark, 32], ranks(metric), 1_000, 1) of
|
||||
{ok, J} -> {ok, J};
|
||||
error -> rank([Mark, 32], ranks(heresy), 1_000, 1)
|
||||
end
|
||||
end.
|
||||
|
||||
rank(Mark, [Mark | _], Magnitude, Sum) ->
|
||||
{ok, Sum * Magnitude};
|
||||
rank(Mark, [_ | Rest], Magnitude, Sum) ->
|
||||
rank(Mark, Rest, Magnitude, Sum * Magnitude);
|
||||
rank(_, [], _, _) ->
|
||||
error.
|
||||
|
||||
|
||||
-spec price_to_string(Pucks) -> Gajus
|
||||
when Pucks :: integer(),
|
||||
Gajus :: string().
|
||||
%% @doc
|
||||
%% A simplified formatting function that converts an integer value in Pucks to a string representation
|
||||
%% in Gajus. Useful for formatting generic output for UI elements
|
||||
|
||||
price_to_string(Pucks) ->
|
||||
Gaju = one(gaju),
|
||||
H = integer_to_list(Pucks div Gaju),
|
||||
R = Pucks rem Gaju,
|
||||
case string:strip(lists:flatten(io_lib:format("~18..0w", [R])), right, $0) of
|
||||
[] -> H;
|
||||
T -> string:join([H, T], ".")
|
||||
end.
|
||||
|
||||
|
||||
-spec string_to_price(Gajus) -> Pucks
|
||||
when Gajus :: string(),
|
||||
Pucks :: integer().
|
||||
%% @doc
|
||||
%% A simplified formatting function that converts a Gaju value represented as a string to an
|
||||
%% integer value in Pucks.
|
||||
|
||||
string_to_price(String) ->
|
||||
case string:split(String, ".") of
|
||||
[H] -> join_price(H, "0");
|
||||
[H, T] -> join_price(H, T);
|
||||
_ -> {error, bad_price}
|
||||
end.
|
||||
|
||||
join_price(H, T) ->
|
||||
try
|
||||
Parts = [H, string:pad(T, 18, trailing, $0)],
|
||||
Price = list_to_integer(unicode:characters_to_list(Parts)),
|
||||
case Price < 0 of
|
||||
false -> {ok, Price};
|
||||
true -> {error, negative_price}
|
||||
end
|
||||
catch
|
||||
error:R -> {error, R}
|
||||
end.
|
||||
+104
-11
@@ -37,17 +37,17 @@
|
||||
%%% @end
|
||||
|
||||
-module(hz_grids).
|
||||
-vsn("0.6.1").
|
||||
-export([url/2, parse/1, req/2, req/3]).
|
||||
-vsn("0.9.2").
|
||||
-export([url/2, url/3, url/4, parse/1, req/2, req/3, req/4]).
|
||||
|
||||
|
||||
-spec url(Instruction, HTTP) -> Result
|
||||
when Instruction :: spend | transfer | sign,
|
||||
HTTP :: uri_string:uri_string(),
|
||||
GRIDS :: uri_string:uri_string(),
|
||||
Result :: {ok, GRIDS} | uri_string:uri_error().
|
||||
Result :: {ok, GRIDS} | uri_string:uri_error(),
|
||||
GRIDS :: uri_string:uri_string().
|
||||
%% @doc
|
||||
%% Takes
|
||||
%% Takes an instruction and an HTTP endpoint location and forms a GRIDS URL.
|
||||
|
||||
url(Instruction, HTTP) ->
|
||||
case uri_string:parse(HTTP) of
|
||||
@@ -66,6 +66,63 @@ url2(Instruction, URL = #{path := Path}) ->
|
||||
{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(),
|
||||
@@ -77,6 +134,8 @@ url2(Instruction, URL = #{path := Path}) ->
|
||||
Amount :: non_neg_integer(),
|
||||
Payload :: binary(),
|
||||
URL :: string().
|
||||
%% @doc
|
||||
%% Translate a GRIDS URL into an Erlang terms instruction.
|
||||
|
||||
parse(GRIDS) ->
|
||||
case uri_string:parse(GRIDS) of
|
||||
@@ -133,27 +192,61 @@ l_to_i(S) ->
|
||||
end.
|
||||
|
||||
|
||||
-spec req(Type, Message) -> Format
|
||||
when Type :: sign | tx | ack,
|
||||
Message :: string() | binary(),
|
||||
Format :: map().
|
||||
%% @doc
|
||||
%% @equiv req(Type, Message, false)
|
||||
|
||||
req(Type, Message) ->
|
||||
req(Type, Message, false).
|
||||
|
||||
req(sign, Message, ID) ->
|
||||
|
||||
-spec req(Type, Message, ID) -> Format
|
||||
when Type :: sign | tx | ack,
|
||||
Message :: string() | binary(),
|
||||
ID :: false | string() | binary(),
|
||||
Format :: map().
|
||||
%% @doc
|
||||
%% Creates a GRIDS message format with the current `NetworkID'.
|
||||
%%
|
||||
%% The `ID' parameter indicates which key the requestee should sign with or
|
||||
%% is `false' to indicate that which key to sign with is up to the requestee.
|
||||
%% @equiv req(Type, Message, ID, CurrentNetworkID)
|
||||
|
||||
req(Type, Message, ID) ->
|
||||
{ok, NetworkID} = hz:network_id(),
|
||||
req(Type, Message, ID, NetworkID).
|
||||
|
||||
|
||||
-spec req(Type, Message, ID, NetworkID) -> Format
|
||||
when Type :: sign | tx | ack,
|
||||
Message :: string() | binary(),
|
||||
ID :: false | string() | binary(),
|
||||
NetworkID :: string() | binary(),
|
||||
Format :: map().
|
||||
%% @doc
|
||||
%% Creates a GRIDS message format.
|
||||
|
||||
req(sign, Message, ID, NetworkID) ->
|
||||
#{"grids" => 1,
|
||||
"chain" => "gajumaru",
|
||||
"network_id" => hz:network_id(),
|
||||
"network_id" => NetworkID,
|
||||
"type" => "message",
|
||||
"public_id" => ID,
|
||||
"payload" => Message};
|
||||
req(tx, Data, ID) ->
|
||||
req(tx, Data, ID, NetworkID) ->
|
||||
#{"grids" => 1,
|
||||
"chain" => "gajumaru",
|
||||
"network_id" => hz:network_id(),
|
||||
"network_id" => NetworkID,
|
||||
"type" => "tx",
|
||||
"public_id" => ID,
|
||||
"payload" => Data};
|
||||
req(ack, Message, ID) ->
|
||||
req(ack, Message, ID, NetworkID) ->
|
||||
#{"grids" => 1,
|
||||
"chain" => "gajumaru",
|
||||
"network_id" => hz:network_id(),
|
||||
"network_id" => NetworkID,
|
||||
"type" => "ack",
|
||||
"public_id" => ID,
|
||||
"payload" => Message}.
|
||||
|
||||
+241
-33
@@ -1,18 +1,43 @@
|
||||
%%% @doc
|
||||
%%% Key functions
|
||||
%%% Hakuzaru Key Functions
|
||||
%%%
|
||||
%%% The main reason this is a module of its own is that in the original architecture
|
||||
%%% it was a process rather than just a library of functions. Now that it exists, though,
|
||||
%%% there is little motivation to cram everything here into the controller process's
|
||||
%%% code.
|
||||
%%% The Gajumaru's default key type is based on Elliptical Curve Cryptography (ECC).
|
||||
%%% The specific curve used is 25519, and the typical key representation is Ed25519.
|
||||
%%%
|
||||
%%% The "Ed" in "Ed25519" stands for Harold Edwards. This form represents
|
||||
%%% a coordinate on a "Twisted Edwards Curve".
|
||||
%%%
|
||||
%%% The "X" in "X25519" stands for the X-coordinate, also known as the
|
||||
%%% "Montgomery u-coordinate" on a "Montgomery Curve".
|
||||
%%%
|
||||
%%% The two are equivalent, but have meaningfully different properties.
|
||||
%%% @end
|
||||
|
||||
-module(hz_key_master).
|
||||
-vsn("0.6.1").
|
||||
-vsn("0.9.2").
|
||||
|
||||
-export([make_key/0, make_key/1, encode/1, decode/1]).
|
||||
-export([shared_secret_a/6, shared_secret_b/6,
|
||||
ed25519_pk_to_x25519/1, ed25519_sk_to_x25519/1,
|
||||
hkdf/4, hkdf/5]).
|
||||
|
||||
|
||||
-export([make_key/1, encode/1, decode/1]).
|
||||
-export([lcg/1]).
|
||||
-spec make_key() -> {ID, KeyPair}
|
||||
when ID :: string(),
|
||||
KeyPair :: #{secret => binary(), public => binary()}.
|
||||
%% @doc
|
||||
%% @equiv make_key(<<>>)
|
||||
|
||||
make_key() ->
|
||||
make_key(<<>>).
|
||||
|
||||
|
||||
-spec make_key(Secret) -> {ID, KeyPair}
|
||||
when Secret :: <<>> | <<_:32*8>>,
|
||||
ID :: string(),
|
||||
KeyPair :: #{secret => binary(), public => binary()}.
|
||||
%% @doc
|
||||
%% Generate a Ed25519 keypair tagged with the corresponding Gajumaru ID.
|
||||
|
||||
make_key(<<>>) ->
|
||||
Pair = #{public := Public} = ecu_eddsa:sign_keypair(),
|
||||
@@ -91,9 +116,8 @@ chunksize(N, C, A) -> chunksize(N div C, C, A + 1).
|
||||
|
||||
|
||||
read_words() ->
|
||||
{ok, V} = zx_lib:string_to_version(proplists:get_value(vsn, module_info(attributes))),
|
||||
HZ_Lib = zx_lib:ppath(lib, {"otpr", "hakuzaru", V}),
|
||||
Path = filename:join([HZ_Lib, "priv", "words4096.txt"]),
|
||||
ModPath = code:which(?MODULE),
|
||||
Path = filename:join([filename:dirname(filename:dirname(ModPath)), "priv", "words4096.txt"]),
|
||||
{ok, Bin} = file:read_file(Path),
|
||||
string:lexemes(Bin, "\n").
|
||||
|
||||
@@ -127,28 +151,212 @@ sumcheck(Width, Bits) ->
|
||||
end.
|
||||
|
||||
|
||||
|
||||
-spec lcg(integer()) -> integer().
|
||||
%% A simple PRNG that fits into 32 bits and is easy to implement anywhere (Kotlin).
|
||||
%% Specifically, it is a "linear congruential generator" of the Lehmer variety.
|
||||
%% The constants used are based on recommendations from Park, Miller and Stockmeyer:
|
||||
%% https://www.firstpr.com.au/dsp/rand31/p105-crawford.pdf#page=4
|
||||
-spec shared_secret_a(A_E_E_SK, B_P_E_PK, B_E_E_PK, Protocol, Version, Salt) -> SS
|
||||
when A_E_E_SK :: binary(),
|
||||
B_P_E_PK :: <<_:32*8>>,
|
||||
B_E_E_PK :: <<_:32*8>>,
|
||||
Protocol :: binary(),
|
||||
Version :: binary(),
|
||||
Salt :: binary(),
|
||||
SS :: <<_:32*8>>.
|
||||
%% @doc
|
||||
%% Alice's side of a shared key derivation based on ed25519 keys as generated by this module.
|
||||
%%
|
||||
%% The input value should be between 1 and 2^31-1.
|
||||
%%
|
||||
%% The purpose of this PRNG is for password-based dictionary shuffling.
|
||||
%% Typically Alice would be providing an ephemeral key to establish
|
||||
%% a shared secret while remaining (at least initially) anonymous from Bob. Bob,
|
||||
%% on the other hand, is providing a permanent key and also an ephemeral key,
|
||||
%% proving identity without exposing the shared secret in the future were one of
|
||||
%% the secrets to be compromised.
|
||||
%% <ul>
|
||||
%% <li>`A_E_E_SK' Alice's Ephemeral Ed25519 Secret Key.</li>
|
||||
%% <li>`B_P_E_PK' Bob's Permanent Ed25519 Public Key.</li>
|
||||
%% <li>`B_E_E_PK' Bob's Ephemeral Ed25519 Public Key.</li>
|
||||
%% <li>`Protocol' is an arbitrary binary string, typically a protocol name in UTF-8.</li>
|
||||
%% <li>`Version' is another arbitrary binary string, typically a protocol version in UTF-8.</li>
|
||||
%% <li>`Salt' is a binary salt, which if empty will be replaced by a binary string of zeroes.</li>
|
||||
%% <li>`SS' is the resulting 32-byte shared secret.</li>
|
||||
%% </ul>
|
||||
|
||||
lcg(N) ->
|
||||
M = 16#7FFFFFFF,
|
||||
A = 48271,
|
||||
Q = 44488, % M div A
|
||||
R = 3399, % M rem A
|
||||
Div = N div Q,
|
||||
Rem = N rem Q,
|
||||
S = Rem * A,
|
||||
T = Div * R,
|
||||
Result = S - T,
|
||||
case Result < 0 of
|
||||
false -> Result;
|
||||
true -> Result + M
|
||||
shared_secret_a(A_E_E_SK, B_P_E_PK, B_E_E_PK, Protocol, Version, Salt) ->
|
||||
A_E_X_SK = ed25519_sk_to_x25519(A_E_E_SK),
|
||||
B_P_X_PK = ed25519_pk_to_x25519(B_P_E_PK),
|
||||
B_E_X_PK = ed25519_pk_to_x25519(B_E_E_PK),
|
||||
DH_Permanent = crypto:compute_key(ecdh, B_P_X_PK, A_E_X_SK, x25519),
|
||||
DH_Ephemeral = crypto:compute_key(ecdh, B_E_X_PK, A_E_X_SK, x25519),
|
||||
finalize_hkdf(DH_Permanent, DH_Ephemeral, Protocol, Version, Salt).
|
||||
|
||||
|
||||
-spec shared_secret_b(B_P_E_SK, B_E_E_SK, A_E_E_PK, Protocol, Version, Salt) -> SS
|
||||
when B_P_E_SK :: binary(),
|
||||
B_E_E_SK :: binary(),
|
||||
A_E_E_PK :: <<_:32*8>>,
|
||||
Protocol :: binary(),
|
||||
Version :: binary(),
|
||||
Salt :: binary(),
|
||||
SS :: <<_:32*8>>.
|
||||
%% @doc
|
||||
%% Bobs's side of a shared key derivation based on ed25519 keys as generated by this module.
|
||||
%%
|
||||
%% Typically Alice would be providing an ephemeral key to establish
|
||||
%% a shared secret while remaining (at least initially) anonymous from Bob. Bob,
|
||||
%% on the other hand, is providing a permanent key and also an ephemeral key,
|
||||
%% proving identity without exposing the shared secret in the future were one of
|
||||
%% the secrets to be compromised.
|
||||
%% <ul>
|
||||
%% <li>`B_P_E_SK' Bob's Permanent Ed25519 Secret Key.</li>
|
||||
%% <li>`B_E_E_SK' Bob's Ephemeral Ed25519 Secret Key.</li>
|
||||
%% <li>`A_E_E_PK' Alice's Ephemeral Ed25519 Public Key.</li>
|
||||
%% <li>`Protocol' is an arbitrary binary string, typically a protocol name in UTF-8.</li>
|
||||
%% <li>`Version' is another arbitrary binary string, typically a protocol version in UTF-8.</li>
|
||||
%% <li>`Salt' is a binary salt, which if empty will be replaced by a binary string of zeroes.</li>
|
||||
%% <li>`SS' is the resulting 32-byte shared secret.</li>
|
||||
%% </ul>
|
||||
|
||||
shared_secret_b(B_P_E_SK, B_E_E_SK, A_E_E_PK, Protocol, Version, Salt) ->
|
||||
B_P_X_SK = ed25519_sk_to_x25519(B_P_E_SK),
|
||||
B_E_X_SK = ed25519_sk_to_x25519(B_E_E_SK),
|
||||
A_E_X_PK = ed25519_pk_to_x25519(A_E_E_PK),
|
||||
DH_Permanent = crypto:compute_key(ecdh, A_E_X_PK, B_P_X_SK, x25519),
|
||||
DH_Ephemeral = crypto:compute_key(ecdh, A_E_X_PK, B_E_X_SK, x25519),
|
||||
finalize_hkdf(DH_Permanent, DH_Ephemeral, Protocol, Version, Salt).
|
||||
|
||||
finalize_hkdf(DH_Permanent, DH_Ephemeral, Protocol, Version, Salt) ->
|
||||
MixedInput = <<DH_Permanent/binary, DH_Ephemeral/binary>>,
|
||||
Info = <<Protocol/binary, ":", Version/binary, ":">>,
|
||||
hkdf(sha256, MixedInput, Salt, Info).
|
||||
|
||||
|
||||
%% Curve25519 Prime Field Constant: 2^255 - 19
|
||||
%% Yes, in hex it reads kind of like "lucky fed"
|
||||
p() -> 16#7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFED.
|
||||
|
||||
|
||||
-spec ed25519_pk_to_x25519(ED25519_PubKey) -> X25519_PubKey
|
||||
when ED25519_PubKey :: <<_:32*8>>,
|
||||
X25519_PubKey :: <<_:32*8>>.
|
||||
%% @doc
|
||||
%% Convert a curve 25519 public key from Edwards representation to X-coordinate
|
||||
%% representation.
|
||||
|
||||
ed25519_pk_to_x25519(<<ED25519_PK:32/binary>>) ->
|
||||
<<CompressedInt:256/little-integer>> = ED25519_PK,
|
||||
% Clear the sign bit (MSB) to get the raw y-coordinate
|
||||
Y = CompressedInt band ((1 bsl 255) - 1),
|
||||
|
||||
% Compute u = (1 + y) / (1 - y) mod P
|
||||
Num = (1 + Y) rem p(),
|
||||
Den = (1 - Y + p()) rem p(),
|
||||
case Den =:= 0 of
|
||||
true ->
|
||||
% If y == 1, the point maps to the point at infinity.
|
||||
% On X25519, this translates to u = 0.
|
||||
<<0:256/little-integer>>;
|
||||
false ->
|
||||
U = (Num * mod_inv(Den, p())) rem p(),
|
||||
<<U:256/little-integer>>
|
||||
end.
|
||||
|
||||
|
||||
-spec ed25519_sk_to_x25519(ED25519_SecKey) -> X25519_SecKey
|
||||
when ED25519_SecKey :: binary(),
|
||||
X25519_SecKey :: <<_:32*8>>.
|
||||
%% @doc
|
||||
%% Convert a curve 25519 secret key from Edwards representation to X-coordinate
|
||||
%% representation.
|
||||
|
||||
ed25519_sk_to_x25519(<<ED25519_SK_Secret:32/binary, _/binary>>) ->
|
||||
<<X25519_SK:32/binary, _/binary>> = crypto:hash(sha512, ED25519_SK_Secret),
|
||||
X25519_SK.
|
||||
|
||||
mod_inv(A, M) ->
|
||||
{1, X, _} = ext_gcd(A, M),
|
||||
(X + M) rem M.
|
||||
|
||||
ext_gcd(A, 0) ->
|
||||
{A, 1, 0};
|
||||
ext_gcd(A, B) ->
|
||||
{G, X1, Y1} = ext_gcd(B, A rem B),
|
||||
{G, Y1, X1 - (A div B) * Y1}.
|
||||
|
||||
|
||||
-spec hkdf(Hash, IKM, Salt, Info) -> DerivedKey
|
||||
when Hash :: md5 | sha | sha224 | sha256 | sha384 | sha512,
|
||||
IKM :: binary(),
|
||||
Salt :: binary(),
|
||||
Info :: binary(),
|
||||
DerivedKey :: <<_:32*8>>.
|
||||
%% @doc
|
||||
%% 32-byte HMAC-Based Extract-and-Expand Key Derivation
|
||||
%% @equiv hkdf(Hash, IKM, Salt, Info, 32)
|
||||
|
||||
hkdf(Hash, IKM, Salt, Info) ->
|
||||
hkdf(Hash, IKM, Salt, Info, 32).
|
||||
|
||||
|
||||
-spec hkdf(Hash, IKM, Salt, Info, Length) -> DerivedKey
|
||||
when Hash :: md5 | sha | sha224 | sha256 | sha384 | sha512,
|
||||
IKM :: binary(),
|
||||
Salt :: binary(),
|
||||
Info :: binary(),
|
||||
Length :: 16 | 20 | 28 | 32 | 48 | 64,
|
||||
DerivedKey :: binary().
|
||||
%% @doc
|
||||
%% RFC-5869 compliant HMAC-Based Extract-and-Expand Key Derivation
|
||||
%%
|
||||
%% RFC-5869:
|
||||
%% <a href="https://datatracker.ietf.org/doc/html/rfc5869">https://datatracker.ietf.org/doc/html/rfc5869</a>
|
||||
%%
|
||||
%% The purpose of HKDF is to take an initial, raw secret input that might
|
||||
%% be mathematically strong but structurally "clumpy" and transform it into one
|
||||
%% or more uniform, high-entropy keys suitable for use in cryptography.
|
||||
%%
|
||||
%% The problem is that when Alice and Bob compute a Diffie-Hellman shared secret
|
||||
%% over X25519, the resulting bytes are mathematically secure, but they are not
|
||||
%% evenly distributed as random noise. Cryptographic ciphers expect keys where
|
||||
%% every single bit has an exactly 50% chance of being a 0 or a 1. Passing raw
|
||||
%% DH outputs straight into a cipher can introduce subtle, exploitable patterns.
|
||||
%%
|
||||
%% HKDF "smooths out" the entropy.
|
||||
%%
|
||||
%% HMAC stands for "Keyed-Hash Message Authentication Code", but without the
|
||||
%% leading "K" just to keep us on our toes. The problem it solves is that simply
|
||||
%% concatenating a secret and some target data and hashing them together to produce
|
||||
%% a message authentication hash leaves the resulting hash vulnerable to a "length
|
||||
%% extension attack". An attacker can append additional data to the end of the
|
||||
%% message and arrive at a valid new hash without ever knowing the secret.
|
||||
%%
|
||||
%% RFC-2104 provides good background information on the technique:
|
||||
%% <a href="https://datatracker.ietf.org/doc/html/rfc2104">https://datatracker.ietf.org/doc/html/rfc2104</a>
|
||||
|
||||
hkdf(Hash, IKM, Salt, Info, Length) ->
|
||||
PRK = extract(Hash, Salt, IKM),
|
||||
expand(Hash, PRK, Info, Length).
|
||||
|
||||
extract(Hash, <<>>, IKM) ->
|
||||
%% If salt is empty RFC 5869 requires a string of zeros equal to hash size
|
||||
Salt = binary:copy(<<0>>, hash_size(Hash)),
|
||||
extract(Hash, Salt, IKM);
|
||||
extract(Hash, Salt, IKM) ->
|
||||
crypto:mac(hmac, Hash, Salt, IKM).
|
||||
|
||||
expand(Hash, PRK, Info, OutLen) ->
|
||||
HashLen = hash_size(Hash),
|
||||
BlockCount = (OutLen + HashLen - 1) div HashLen,
|
||||
true = BlockCount =< 255,
|
||||
FullBlocks = expand_loop(Hash, PRK, Info, BlockCount, 1, <<>>, <<>>),
|
||||
<<Output:OutLen/binary, _/binary>> = FullBlocks,
|
||||
Output.
|
||||
|
||||
expand_loop(Hash, PRK, Info, N, Counter, PrevT, Acc) when Counter =< N ->
|
||||
Payload = <<PrevT/binary, Info/binary, Counter:8>>,
|
||||
T = crypto:mac(hmac, Hash, PRK, Payload),
|
||||
expand_loop(Hash, PRK, Info, N, Counter + 1, T, <<Acc/binary, T/binary>>);
|
||||
expand_loop(_, _, _, _, _, _, Acc) ->
|
||||
Acc.
|
||||
|
||||
hash_size(md5) -> 16;
|
||||
hash_size(sha) -> 20;
|
||||
hash_size(sha224) -> 28;
|
||||
hash_size(sha256) -> 32;
|
||||
hash_size(sha384) -> 48;
|
||||
hash_size(sha512) -> 64.
|
||||
|
||||
+116
-28
@@ -9,7 +9,7 @@
|
||||
%%% @end
|
||||
|
||||
-module(hz_man).
|
||||
-vsn("0.6.1").
|
||||
-vsn("0.9.2").
|
||||
-behavior(gen_server).
|
||||
-author("Craig Everett <ceverett@tsuriai.jp>").
|
||||
-copyright("Craig Everett <ceverett@tsuriai.jp>").
|
||||
@@ -20,17 +20,17 @@
|
||||
chain_nodes/0, chain_nodes/1,
|
||||
timeout/0, timeout/1]).
|
||||
|
||||
%% Contract caching
|
||||
-export([cache_aaci/2, lookup_aaci/1]).
|
||||
|
||||
%% The whole point of this module:
|
||||
-export([request/1, request/2]).
|
||||
-export([request_sticky/1, request_sticky/2, request/1, request/2]).
|
||||
|
||||
%% gen_server goo
|
||||
-export([start_link/0]).
|
||||
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
|
||||
code_change/3, terminate/2]).
|
||||
|
||||
%% TODO: Make logging more flexible
|
||||
-include("$zx_include/zx_logger.hrl").
|
||||
|
||||
|
||||
%%% Type and Record Definitions
|
||||
|
||||
@@ -43,11 +43,12 @@
|
||||
req = none :: none | binary()}).
|
||||
|
||||
-record(s,
|
||||
{tls = false :: boolean(),
|
||||
chain_nodes = {[], []} :: {[hz:chain_node()], [hz:chain_node()]},
|
||||
sticky = none :: none | hz:chain_node(),
|
||||
fetchers = [] :: [#fetcher{}],
|
||||
timeout = 5000 :: pos_integer()}).
|
||||
{tls = false :: boolean(),
|
||||
chain_nodes = {[], []} :: {[hz:chain_node()], [hz:chain_node()]},
|
||||
sticky = none :: none | hz:chain_node(),
|
||||
fetchers = [] :: [#fetcher{}],
|
||||
timeout = 5000 :: pos_integer(),
|
||||
cache = #{} :: #{Label :: term() := AACI :: hz:aaci()}}).
|
||||
|
||||
|
||||
-type state() :: #s{}.
|
||||
@@ -97,6 +98,41 @@ timeout(Value) when 0 < Value, Value =< 120000 ->
|
||||
gen_server:cast(?MODULE, {timeout, Value}).
|
||||
|
||||
|
||||
-spec cache_aaci(Label, AACI) -> ok
|
||||
when Label :: term(),
|
||||
AACI :: hz:aaci().
|
||||
|
||||
cache_aaci(Label, AACI) ->
|
||||
gen_server:call(?MODULE, {cache, Label, AACI}).
|
||||
|
||||
|
||||
-spec lookup_aaci(Label) -> Result
|
||||
when Label :: term(),
|
||||
Result :: {ok, hz:aaci()} | error.
|
||||
|
||||
lookup_aaci(Label) ->
|
||||
gen_server:call(?MODULE, {lookup, Label}).
|
||||
|
||||
|
||||
-spec request_sticky(Path) -> {ok, Value} | {error, Reason}
|
||||
when Path :: unicode:charlist(),
|
||||
Value :: map(),
|
||||
Reason :: hz:chain_error().
|
||||
|
||||
request_sticky(Path) ->
|
||||
gen_server:call(?MODULE, {request_sticky, {get, Path}}, infinity).
|
||||
|
||||
|
||||
-spec request_sticky(Path, Data) -> {ok, Value} | {error, Reason}
|
||||
when Path :: unicode:charlist(),
|
||||
Data :: unicode:charlist(),
|
||||
Value :: map(),
|
||||
Reason :: hz:chain_error().
|
||||
|
||||
request_sticky(Path, Data) ->
|
||||
gen_server:call(?MODULE, {request_sticky, {post, Path, Data}}, infinity).
|
||||
|
||||
|
||||
-spec request(Path) -> {ok, Value} | {error, Reason}
|
||||
when Path :: unicode:charlist(),
|
||||
Value :: map(),
|
||||
@@ -136,7 +172,6 @@ start_link() ->
|
||||
%% preparatory work necessary for proper function.
|
||||
|
||||
init(none) ->
|
||||
ok = io:format("hz_man starting.~n"),
|
||||
State = #s{},
|
||||
{ok, State}.
|
||||
|
||||
@@ -148,10 +183,19 @@ init(none) ->
|
||||
handle_call({request, Request}, From, State) ->
|
||||
NewState = do_request(Request, From, State),
|
||||
{noreply, NewState};
|
||||
handle_call({request_sticky, Request}, From, State) ->
|
||||
NewState = do_request_sticky(Request, From, State),
|
||||
{noreply, NewState};
|
||||
handle_call({lookup, Label}, _, State) ->
|
||||
Result = do_lookup(Label, State),
|
||||
{reply, Result, State};
|
||||
handle_call({cache, Label, AACI}, _, State) ->
|
||||
NewState = do_cache_aaci(Label, AACI, State),
|
||||
{reply, ok, NewState};
|
||||
handle_call(tls, _, State = #s{tls = TLS}) ->
|
||||
{reply, TLS, State};
|
||||
handle_call(chain_nodes, _, State = #s{chain_nodes = {Wait, Used}}) ->
|
||||
Nodes = lists:append(Wait, Used),
|
||||
handle_call(chain_nodes, _, State) ->
|
||||
Nodes = do_chain_nodes(State),
|
||||
{reply, Nodes, State};
|
||||
handle_call(timeout, _, State = #s{timeout = Value}) ->
|
||||
{reply, Value, State};
|
||||
@@ -163,10 +207,9 @@ handle_call(Unexpected, From, State) ->
|
||||
handle_cast({tls, Boolean}, State) ->
|
||||
NewState = do_tls(Boolean, State),
|
||||
{noreply, NewState};
|
||||
handle_cast({chain_nodes, []}, State) ->
|
||||
{noreply, State#s{chain_nodes = {[], []}}};
|
||||
handle_cast({chain_nodes, ToUse}, State) ->
|
||||
{noreply, State#s{chain_nodes = {ToUse, []}}};
|
||||
handle_cast({chain_nodes, List}, State) ->
|
||||
NewState = do_chain_nodes(List, State),
|
||||
{noreply, NewState};
|
||||
handle_cast({timeout, Value}, State) ->
|
||||
{noreply, State#s{timeout = Value}};
|
||||
handle_cast(Unexpected, State) ->
|
||||
@@ -221,6 +264,23 @@ terminate(_, _) ->
|
||||
|
||||
%%% Doer Functions
|
||||
|
||||
do_chain_nodes(#s{sticky = none, chain_nodes = {Wait, Used}}) ->
|
||||
lists:append(Wait, Used);
|
||||
do_chain_nodes(#s{sticky = Sticky, chain_nodes = {Wait, Used}}) ->
|
||||
case lists:append(Wait, Used) of
|
||||
[Sticky] -> [Sticky];
|
||||
Nodes -> [Sticky | Nodes]
|
||||
end.
|
||||
|
||||
|
||||
do_chain_nodes([], State) ->
|
||||
State#s{sticky = none, chain_nodes = {[], []}};
|
||||
do_chain_nodes(List = [Sticky], State) ->
|
||||
State#s{sticky = Sticky, chain_nodes = {List, []}};
|
||||
do_chain_nodes([Sticky | List], State) ->
|
||||
State#s{sticky = Sticky, chain_nodes = {List, []}}.
|
||||
|
||||
|
||||
do_tls(true, State) ->
|
||||
ok = ssl:start(),
|
||||
State#s{tls = true};
|
||||
@@ -230,17 +290,30 @@ do_tls(_, State) ->
|
||||
State.
|
||||
|
||||
|
||||
do_request(_, From, State = #s{chain_nodes = {[], []}}) ->
|
||||
do_cache_aaci(Label, AACI, State = #s{cache = Cache}) ->
|
||||
NewCache = maps:put(Label, AACI, Cache),
|
||||
State#s{cache = NewCache}.
|
||||
|
||||
|
||||
do_lookup(Label, #s{cache = Cache}) ->
|
||||
maps:find(Label, Cache).
|
||||
|
||||
|
||||
do_request_sticky(_, From, State = #s{sticky = none}) ->
|
||||
ok = gen_server:reply(From, {error, no_nodes}),
|
||||
State;
|
||||
do_request(Request,
|
||||
From,
|
||||
State = #s{tls = false,
|
||||
fetchers = Fetchers,
|
||||
chain_nodes = {[Node | Rest], Used},
|
||||
timeout = Timeout}) ->
|
||||
do_request_sticky(Request,
|
||||
From,
|
||||
State = #s{tls = TLS,
|
||||
fetchers = Fetchers,
|
||||
sticky = Node,
|
||||
timeout = Timeout}) ->
|
||||
Now = erlang:system_time(nanosecond),
|
||||
Fetcher = fun() -> hz_fetcher:connect(Node, Request, From, Timeout) end,
|
||||
Fetcher =
|
||||
case TLS of
|
||||
true -> fun() -> hz_fetcher:connect_slowly(Node, Request, From, Timeout) end;
|
||||
false -> fun() -> hz_fetcher:connect(Node, Request, From, Timeout) end
|
||||
end,
|
||||
{PID, Mon} = spawn_monitor(Fetcher),
|
||||
New = #fetcher{pid = PID,
|
||||
mon = Mon,
|
||||
@@ -248,15 +321,24 @@ do_request(Request,
|
||||
node = Node,
|
||||
from = From,
|
||||
req = Request},
|
||||
State#s{fetchers = [New | Fetchers], chain_nodes = {Rest, [Node | Used]}};
|
||||
State#s{fetchers = [New | Fetchers]}.
|
||||
|
||||
|
||||
do_request(_, From, State = #s{chain_nodes = {[], []}}) ->
|
||||
ok = gen_server:reply(From, {error, no_nodes}),
|
||||
State;
|
||||
do_request(Request,
|
||||
From,
|
||||
State = #s{tls = true,
|
||||
State = #s{tls = TLS,
|
||||
fetchers = Fetchers,
|
||||
chain_nodes = {[Node | Rest], Used},
|
||||
timeout = Timeout}) ->
|
||||
Now = erlang:system_time(nanosecond),
|
||||
Fetcher = fun() -> hz_fetcher:slowly_connect(Node, Request, From, Timeout) end,
|
||||
Fetcher =
|
||||
case TLS of
|
||||
true -> fun() -> hz_fetcher:connect_slowly(Node, Request, From, Timeout) end;
|
||||
false -> fun() -> hz_fetcher:connect(Node, Request, From, Timeout) end
|
||||
end,
|
||||
{PID, Mon} = spawn_monitor(Fetcher),
|
||||
New = #fetcher{pid = PID,
|
||||
mon = Mon,
|
||||
@@ -268,3 +350,9 @@ do_request(Request,
|
||||
do_request(Request, From, State = #s{chain_nodes = {[], Used}}) ->
|
||||
Fresh = lists:reverse(Used),
|
||||
do_request(Request, From, State#s{chain_nodes = {Fresh, []}}).
|
||||
|
||||
|
||||
log(Level, Format, Args) ->
|
||||
Raw = io_lib:format("~w ~w: " ++ Format, [?MODULE, self() | Args]),
|
||||
Entry = unicode:characters_to_list(Raw),
|
||||
logger:log(Level, Entry).
|
||||
|
||||
+1521
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -9,7 +9,7 @@
|
||||
%%% @end
|
||||
|
||||
-module(hz_sup).
|
||||
-vsn("0.6.1").
|
||||
-vsn("0.9.2").
|
||||
-behaviour(supervisor).
|
||||
-author("Craig Everett <zxq9@zxq9.com>").
|
||||
-copyright("Craig Everett <zxq9@zxq9.com>").
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
{type,app}.
|
||||
{modules,[]}.
|
||||
{prefix,"hz"}.
|
||||
{desc,"Gajumaru interoperation library"}.
|
||||
{author,"Craig Everett"}.
|
||||
{package_id,{"otpr","hakuzaru",{0,6,1}}}.
|
||||
{desc,"Gajumaru interoperation library"}.
|
||||
{package_id,{"otpr","hakuzaru",{0,9,2}}}.
|
||||
{deps,[{"otpr","sophia",{9,0,0}},
|
||||
{"otpr","gmserialization",{0,1,3}},
|
||||
{"otpr","gmbytecode",{3,4,1}},
|
||||
@@ -19,6 +19,6 @@
|
||||
{copyright,"Craig Everett"}.
|
||||
{file_exts,[]}.
|
||||
{license,"MIT"}.
|
||||
{repo_url,"https://gitlab.com/ioecs/hakuzaru"}.
|
||||
{repo_url,"https://git.qpq.swiss/QPQ-AG/hakuzaru"}.
|
||||
{tags,["qpq","gajumaru","blockchain","hakuzaru","crypto","defi"]}.
|
||||
{ws_url,"https://gitlab.com/ioecs/hakuzaru"}.
|
||||
{ws_url,"https://git.qpq.swiss/QPQ-AG/hakuzaru"}.
|
||||
|
||||
Reference in New Issue
Block a user