diff --git a/README.md b/README.md
index ca49030..9b67b1c 100644
--- a/README.md
+++ b/README.md
@@ -16,10 +16,6 @@ Vanillae is just a knife.
## Flagship Tools
-- Jaeck Russell
-
- A minimal browser wallet extension. Not included in this repository yet.
-
- [Sidekick](./sidekick/)
A library to talk to a browser wallet extension from the perspective of a
@@ -49,25 +45,6 @@ out.
consist of what data the waellet expects from the aepp, and what data the
aepp expects from the waellet.
-- [Parasite](./libs/parasite/)
-
- **NOT FOR PRODUCTION USE**
-
- This is a library for talking to Aeternity HTTP nodes from the perspective
- of a page script. Used in example/documentation code for things that your
- backend should do.
-
- This may eventually be polished and repackaged as a production quality
- library.
-
-- libjr
-
- This is a library to talk to a page script ("aepp") from the perspective of
- the wallet ("waellet"). It essentially is sidekick from the perspective of
- the wallet.
-
- Not yet included in this repository.
-
## Utilities
diff --git a/bindings/erlang/src/vanillae.erl b/bindings/erlang/src/vanillae.erl
index 50d64da..6703a2f 100644
--- a/bindings/erlang/src/vanillae.erl
+++ b/bindings/erlang/src/vanillae.erl
@@ -40,31 +40,39 @@
% AE node JSON query interface functions
-export([top_height/0, top_block/0,
kb_current/0, kb_current_hash/0, kb_current_height/0,
-% kb_pending/0,
+ kb_pending/0,
kb_by_hash/1, kb_by_height/1,
% kb_insert/1,
mb_header/1, mb_txs/1, mb_tx_index/2, mb_tx_count/1,
gen_current/0, gen_by_id/1, gen_by_height/1,
acc/1, acc_at_height/2, acc_at_block_id/2,
-% acc_pending_txs/1,
+ acc_pending_txs/1,
next_nonce/1,
- dry_run/1, dry_run/2,
+ dry_run/1, dry_run/2, dry_run/3,
tx/1, tx_info/1,
post_tx/1,
contract/1, contract_code/1,
-% contract_poi/1,
+ contract_poi/1,
% oracle/1, oracle_queries/1, oracle_queries_by_id/2,
name/1,
% channel/1,
peer_pubkey/0,
- status/0]).
-% status_chainends/0]).
+ status/0,
+ status_chainends/0]).
% AE contract call and serialization interface functions
-export([read_aci/1,
+ min_gas/0,
+ min_gas_price/0,
+ min_fee/0,
+ contract_create/3,
+ contract_create/8,
prepare_contract/1,
+ contract_call/5,
contract_call/6,
- contract_call/10]).
+ contract_call/10,
+ verify_signature/3]).
+
% OTP Application Interface
%-export([start/0, stop/0]).
@@ -73,7 +81,7 @@
%%% Types
--export_type([ae_node/0, network_id/0]).
+-export_type([ae_node/0, network_id/0, ae_error/0]).
-type ae_node() :: {inet:ip_address(), inet:port_number()}.
@@ -88,9 +96,9 @@
| {headers, map()}
| bad_length
| gc_out_of_range.
--type pubkey() :: string(). % "ak_" ++ _
+-type pubkey() :: unicode:chardata(). % "ak_" ++ _
-type account_id() :: pubkey().
--type contract_id() :: string(). % "ct_" ++ _
+-type contract_id() :: unicode:chardata(). % "ct_" ++ _
-type peer_pubkey() :: string(). % "pp_" ++ _
-type keyblock_hash() :: string(). % "kh_" ++ _
-type contract_byte_array() :: string(). % "cb_" ++ _
@@ -134,7 +142,20 @@
% "block_height" => pos_integer(),
% "hash" => tx_hash(),
% "signatures" => [signature()],
-% "tx" => map()}. % FIXME
+% "tx" =>
+% #{"abi_version" => pos_integer(),
+% "amount" => non_neg_integer(),
+% "call_data" => contract_byte_array(),
+% "code" => contract_byte_array(),
+% "deposit" => non_neg_integer(),
+% "fee" => pos_integer(),
+% "gas" => pos_integer(),
+% "gas_price" => pos_integer(),
+% "nonce" => pos_integer(),
+% "owner_id" => account_id(),
+% "type" => string(),
+% "version" => pos_integer(),
+% "vm_version" => pos_integer()}}
-type generation() :: #{string() => term()}.
% #{"key_block" => keyblock(),
% "micro_blocks" => [microblock_hash()]}.
@@ -321,10 +342,15 @@ kb_current_height() ->
end.
-%-spec kb_pending() ->
-%
-%kb_pending() ->
-% request("/v2/key-blocks/pending").
+-spec kb_pending() -> {ok, keyblock_hash()} | {error, Reason}
+ when Reason :: string().
+%% @doc
+%% Request the hash of the pending keyblock of a mining node's beneficiary.
+%% If the node queried is not configured for mining it will return
+%% `{error, "Beneficiary not configured"}'
+
+kb_pending() ->
+ result(request("/v2/key-blocks/pending")).
-spec kb_by_hash(ID) -> {ok, KeyBlock} | {error, Reason}
@@ -488,16 +514,15 @@ acc_at_block_id(AccountID, BlockID) ->
end.
-% TODO
-%-spec acc_pending_txs(AccountID) -> {ok, TXs} | {error, Reason}
-% when AccountID :: account_id(),
-% TXs ::
-% Reason ::
-%%% @doc
-%%% Retrieve a list of transactions pending for the given account.
-%
-%acc_pending_txs(AccountID) ->
-% request(["/v2/accounts/", AccountID, "/transactions/pending"]).
+-spec acc_pending_txs(AccountID) -> {ok, TXs} | {error, Reason}
+ when AccountID :: account_id(),
+ TXs :: [tx_hash()],
+ Reason :: ae_error() | string().
+%% @doc
+%% Retrieve a list of transactions pending for the given account.
+
+acc_pending_txs(AccountID) ->
+ request(["/v2/accounts/", AccountID, "/transactions/pending"]).
-spec next_nonce(AccountID) -> {ok, Nonce} | {error, Reason}
@@ -508,8 +533,14 @@ acc_at_block_id(AccountID, BlockID) ->
%% Retrieve the next nonce for the given account
next_nonce(AccountID) ->
- case request(["/v2/accounts/", AccountID, "/next-nonce"]) of
- {ok, #{"next_nonce" := Nonce}} -> {ok, Nonce};
+% case request(["/v2/accounts/", AccountID, "/next-nonce"]) of
+% {ok, #{"next_nonce" := Nonce}} -> {ok, Nonce};
+% {ok, #{"reason" := "Account not found"}} -> {ok, 1};
+% {ok, #{"reason" := Reason}} -> {error, Reason};
+% Error -> Error
+% end.
+ case request(["/v2/accounts/", AccountID]) of
+ {ok, #{"nonce" := Nonce}} -> {ok, Nonce + 1};
{ok, #{"reason" := "Account not found"}} -> {ok, 1};
{ok, #{"reason" := Reason}} -> {error, Reason};
Error -> Error
@@ -527,27 +558,45 @@ next_nonce(AccountID) ->
%% {ok, Hash} = vanillae:kb_current_hash(),
%% vanilla:dry_run(TX, Hash),
%% '''
+%% NOTE:
+%% For this function to work the Aeternity node you are sending the request
+%% to must have its configuration set to `http: endpoints: dry-run: true'
dry_run(TX) ->
+ dry_run(TX, []).
+
+
+-spec dry_run(TX, Accounts) -> {ok, Result} | {error, Reason}
+ when TX :: binary() | string(),
+ Accounts :: [pubkey()],
+ Result :: term(), % FIXME
+ Reason :: term(). % FIXME
+
+dry_run(TX, Accounts) ->
case kb_current_hash() of
- {ok, Hash} -> dry_run(TX, Hash);
+ {ok, Hash} -> dry_run(TX, Accounts, Hash);
Error -> Error
end.
--spec dry_run(TX, KBHash) -> {ok, Result} | {error, Reason}
- when TX :: binary() | string(),
- KBHash :: binary() | string(),
- Result :: term(), % FIXME
- Reason :: term(). % FIXME
+-spec dry_run(TX, Accounts, KBHash) -> {ok, Result} | {error, Reason}
+ when TX :: binary() | string(),
+ Accounts :: [pubkey()],
+ KBHash :: binary() | string(),
+ Result :: term(), % FIXME
+ Reason :: term(). % FIXME
%% @doc
%% Execute a read-only transaction on the chain at the height indicated by the
%% hash provided.
-dry_run(TX, KBHash) ->
+dry_run(TX, Accounts, KBHash) ->
KBB = to_binary(KBHash),
TXB = to_binary(TX),
- JSON = zj:binary_encode(#{top => KBB, accounts => [], txs => [#{tx => TXB}]}),
+ DryData = #{top => KBB,
+ accounts => Accounts,
+ txs => [#{tx => TXB}],
+ tx_events => true},
+ JSON = zj:binary_encode(DryData),
request("/v2/dry-run", JSON).
to_binary(S) when is_binary(S) -> S;
@@ -584,7 +633,8 @@ tx_info(ID) ->
%% Post a transaction to the chain.
post_tx(Data) ->
- request("/v2/transactions", Data).
+ JSON = zj:binary_encode(#{tx => Data}),
+ request("/v2/transactions", JSON).
-spec contract(ID) -> {ok, ContractData} | {error, Reason}
@@ -611,11 +661,13 @@ contract_code(ID) ->
end.
-% FIXME: Is this broken? Seems to just stall
-% -spec conract_poi(ID) ->
-%
-%contract_poi(ID) ->
-% request(["/v2/contracts/", ID, "/poi"]).
+-spec contract_poi(ID) -> {ok, Bytecode} | {error, Reason}
+ when ID :: contract_id(),
+ Bytecode :: contract_byte_array(),
+ Reason :: ae_error() | string().
+
+contract_poi(ID) ->
+ request(["/v2/contracts/", ID, "/poi"]).
% TODO
%oracle(ID) ->
@@ -675,24 +727,23 @@ status() ->
request("/v2/status").
-% TODO
-%-spec status_chainends() -> {ok, ChainEnds} | {error, Reason}
-% when ChainEnds :: [keyblock_hash()],
-% Reason :: ae_error().
-%%% @doc
-%%% Retrieve the latest keyblock hashes
-%
-%status_chainends() ->
-% request("/v2/status/chain-ends").
+-spec status_chainends() -> {ok, ChainEnds} | {error, Reason}
+ when ChainEnds :: [keyblock_hash()],
+ Reason :: ae_error().
+%% @doc
+%% Retrieve the latest keyblock hashes
+
+status_chainends() ->
+ request("/v2/status/chain-ends").
request(Path) ->
- vanillae_man:request(Path).
+ vanillae_man:request(unicode:characters_to_list(Path)).
request(Path, Payload) ->
- vanillae_man:request(Path, Payload).
-
+ vanillae_man:request(unicode:characters_to_list(Path), Payload).
+
result({ok, #{"reason" := Reason}}) -> {error, Reason};
result(Received) -> Received.
@@ -701,6 +752,236 @@ result(Received) -> Received.
%%% Contract calls
+-spec contract_create(CreatorID, Path, InitArgs) -> Result
+ when CreatorID :: unicode:chardata(),
+ Path :: file:filename(),
+ InitArgs :: [string()],
+ Result :: {ok, CreateTX} | {error, Reason},
+ CreateTX :: binary(),
+ Reason :: file:posix() | term().
+%% @doc
+%% This function reads the source of a Sophia contract (an .aes file)
+%% and returns the unsigned create contract call data with default values.
+%% For more control over exactly what those values are, use create_contract/8.
+
+contract_create(CreatorID, Path, InitArgs) ->
+ case next_nonce(CreatorID) of
+ {ok, Nonce} ->
+ Amount = 0,
+ Gas = 100000,
+ GasPrice = min_gas_price(),
+ Fee = min_fee(),
+ contract_create(CreatorID, Nonce,
+ Amount, Gas, GasPrice, Fee,
+ Path, InitArgs);
+ Error ->
+ Error
+ end.
+
+
+-spec contract_create(CreatorID, Nonce,
+ Amount, Gas, GasPrice, Fee,
+ Path, InitArgs) -> Result
+ when CreatorID :: unicode:chardata(),
+ Nonce :: pos_integer(),
+ Amount :: non_neg_integer(),
+ Gas :: pos_integer(),
+ GasPrice :: pos_integer(),
+ Fee :: non_neg_integer(),
+ Path :: file:filename(),
+ InitArgs :: [string()],
+ Result :: {ok, CreateTX} | {error, Reason},
+ CreateTX :: binary(),
+ Reason :: term().
+%% @doc
+%% Create a "create contract" call using the supplied values.
+%%
+%% Contract creation is an even more opaque process than contract calls if you're new
+%% to Aeternity.
+%%
+%% The meaning of each argument is as follows:
+%%
+%% -
+%% CreatorID:
+%% This is the public key of the entity who will be posting the contract
+%% to the chain.
+%% The key must be encoded as a binary string prefixed with <<"ak_">>.
+%% The returned call will still need to be signed by the caller's private
+%% key.
+%%
+%% -
+%% Nonce:
+%% This is a sequential integer value that ensures that the hash value of two
+%% sequential signed calls with the same contract ID, function and arguments can
+%% never be the same.
+%% This avoids replay attacks and ensures indempotency despite the distributed
+%% nature of the blockchain network).
+%% Every CallerID on the chain has a "next nonce" value that can be discovered by
+%% querying your Aeternity node (via `vanillae:next_nonce(CallerID)', for example).
+%%
+%% -
+%% Amount:
+%% All Aeternity transactions can carry an "amount" spent from the origin account
+%% (in this case the `CallerID') to the destination. In a "Spend" transaction this
+%% is the only value that really matters, but in a contract call the utility is
+%% quite different, as you can pay money into a contract and have that
+%% contract hold it (for future payouts, to be held in escrow, as proof of intent
+%% to purchase or engage in an auction, whatever). Typically this value is 0, but
+%% of course there are very good reasons why it should be set to a non-zero value
+%% in the case of calls related to contract-governed payment systems.
+%%
+%% -
+%% Gas:
+%% This number sets a limit on the maximum amount of computation the caller is willing
+%% to pay for on the chain.
+%% Both storage and thunks are costly as the entire Aeternity network must execute,
+%% verify, store and replicate all state changes to the chain.
+%% Each byte stored on the chain carries a cost of 20 gas, which is not an issue if
+%% you are storing persistent values of some state trasforming computation, but
+%% high enough to discourage frivolous storage of media on the chain (which would be
+%% a burden to the entire network).
+%% Computation is less expensive, but still costs and is calculated very similarly
+%% to the Erlang runtime's per-process reduction budget.
+%% The maximum amount of gas that a microblock is permitted to carry (its maximum
+%% computational weight, so to speak) is 6,000,000.
+%% Typical contract calls range between about 100 to 15,000 gas, so the default gas
+%% limit set by the `contract_call/6' function is only 20,000.
+%% Setting the gas limit to 6,000,000 or more will cause your contract call to fail.
+%% All transactions cost some gas with the exception of stateless or read-only
+%% calls to your Aeternity node (executed as "dry run" calls and not propagated to
+%% the network).
+%% The gas consumed by the contract call transaction is multiplied by the `GasPrice'
+%% provided and rolled into the block reward paid out to the node that mines the
+%% transaction into a microblock.
+%% Unused gas is refunded to the caller.
+%%
+%% -
+%% GasPrice:
+%% This is a factor that is used calculate a value in aettos (the smallest unit of
+%% Aeternity's currency value) for the gas consumed. In times of high contention
+%% in the mempool increasing the gas price increases the value of mining a given
+%% transaction, thus making miners more likely to prioritize the high value ones.
+%%
+%% -
+%% Fee:
+%% This value should really be caled `Bribe' or `Tip'.
+%% This is a flat fee in aettos that is paid into the block reward, thereby allowing
+%% an additional way to prioritize a given transaction above others, even if the
+%% transaction will not consume much gas.
+%%
+%% -
+%% ACI:
+%% This is the compiled contract's metadata. It provides the information necessary
+%% for the contract call data to be formed in a way that the Aeternity runtime will
+%% understand.
+%% This ACI data must be already formatted in the native Erlang format as an .aci
+%% file rather than as the JSON serialized format produced by the Sophia CLI tool.
+%% The easiest way to create native ACI data is to use the Aeternity Launcher,
+%% a GUI tool with a "Developers' Workbench" feature that can assist with this.
+%%
+%% -
+%% ConID:
+%% This is the on-chain address of the contract instance that is to be called.
+%% Note, this is different from the `name' of the contract, as a single contract may
+%% be deployed multiple times.
+%%
+%% -
+%% Fun:
+%% This is the name of the entrypoint function to be called on the contract,
+%% provided as a string (not a binary string, but a textual string as a list).
+%%
+%% -
+%% Args:
+%% This is a list of the arguments to provide to the function, listed in order
+%% according to the function's spec, and represented as strings (that is, an integer
+%% argument of `10' must be cast to the textual representation `"10"').
+%%
+%% '''
+%% As should be obvious from the above description, it is pretty helpful to have a
+%% source copy of the contract you intend to call so that you can re-generate the ACI
+%% if you do not already have a copy, and can check the spec of a function before
+%% trying to form a contract call.
+
+contract_create(CreatorID, Nonce,
+ Amount, Gas, GasPrice, Fee,
+ Path, InitArgs) ->
+ case aeso_compiler:file(Path, [{aci, json}]) of
+ {ok, Compiled} ->
+ contract_create2(CreatorID, Nonce,
+ Amount, Gas, GasPrice, Fee,
+ Compiled, InitArgs);
+ Error ->
+ Error
+ end.
+
+contract_create2(CreatorID, Nonce,
+ Amount, Gas, GasPrice, Fee,
+ Compiled, InitArgs) ->
+ AACI = prepare_aaci(maps:get(aci, Compiled)),
+ case encode_call_data(AACI, "init", InitArgs) of
+ {ok, CallData} ->
+ contract_create3(CreatorID, Nonce,
+ Amount, Gas, GasPrice, Fee,
+ Compiled, CallData);
+ Error ->
+ Error
+ end.
+
+contract_create3(CreatorID, Nonce,
+ Amount, Gas, GasPrice, Fee,
+ Compiled, CallData) ->
+ PK = unicode:characters_to_binary(CreatorID),
+ try
+ {account_pubkey, OwnerID} = aeser_api_encoder:decode(PK),
+ contract_create4(OwnerID, Nonce,
+ Amount, Gas, GasPrice, Fee,
+ Compiled, CallData)
+ catch
+ Error:Reason -> {Error, Reason}
+ end.
+
+contract_create4(OwnerID, Nonce,
+ Amount, Gas, GasPrice, Fee,
+ Compiled, CallData) ->
+ Code = aeser_contract_code:serialize(Compiled),
+ VM = 7,
+ ABI = 3,
+ <> = <>,
+ ContractCreateVersion = 1,
+ TTL = 0,
+ Type = contract_create_tx,
+ Fields =
+ [{owner_id, aeser_id:create(account, OwnerID)},
+ {nonce, Nonce},
+ {code, Code},
+ {ct_version, CTVersion},
+ {fee, Fee},
+ {ttl, TTL},
+ {deposit, 0},
+ {amount, Amount},
+ {gas, Gas},
+ {gas_price, GasPrice},
+ {call_data, CallData}],
+ Template =
+ [{owner_id, id},
+ {nonce, int},
+ {code, binary},
+ {ct_version, int},
+ {fee, int},
+ {ttl, int},
+ {deposit, int},
+ {amount, int},
+ {gas, int},
+ {gas_price, int},
+ {call_data, binary}],
+ TXB = aeser_chain_objects:serialize(Type, ContractCreateVersion, Template, Fields),
+ try
+ {ok, aeser_api_encoder:encode(transaction, TXB)}
+ catch
+ error:Reason -> {error, Reason}
+ end.
+
+
-spec read_aci(Path) -> Result
when Path :: file:filename(),
Result :: {ok, ACI} | {error, Reason},
@@ -733,14 +1014,15 @@ read_aci(Path) ->
end.
--spec contract_call(CallerID, Nonce, ACI, ConID, Fun, Args) -> CallTX
- when CallerID :: binary(),
- Nonce :: pos_integer(),
- ACI :: binary(),
- ConID :: binary(),
+-spec contract_call(CallerID, AACI, ConID, Fun, Args) -> Result
+ when CallerID :: unicode:chardata(),
+ AACI :: map(),
+ ConID :: unicode:chardata(),
Fun :: string(),
Args :: [string()],
- CallTX :: string().
+ Result :: {ok, CallTX} | {error, Reason},
+ CallTX :: binary(),
+ Reason :: term().
%% @doc
%% Form a contract call using hardcoded default values for `Gas', `GasPrice', `Fee',
%% and `Amount' to simplify the call (10 args is a bit much for normal calls!).
@@ -750,30 +1032,60 @@ read_aci(Path) ->
%% For details on the meaning of these and other argument values see the doc comment
%% for contract_call/10.
-contract_call(CallerID, Nonce, ACI, ConID, Fun, Args) ->
- Gas = 20000,
+contract_call(CallerID, AACI, ConID, Fun, Args) ->
+ {ok, Nonce} = next_nonce(CallerID),
+ Gas = min_gas(),
GasPrice = min_gas_price(),
- Fee = 20000,
+ Fee = min_fee(),
Amount = 0,
contract_call(CallerID, Nonce,
Gas, GasPrice, Fee, Amount,
- ACI, ConID, Fun, Args).
+ AACI, ConID, Fun, Args).
+
+
+-spec contract_call(CallerID, Gas, AACI, ConID, Fun, Args) -> Result
+ when CallerID :: unicode:chardata(),
+ Gas :: pos_integer(),
+ AACI :: map(),
+ ConID :: unicode:chardata(),
+ Fun :: string(),
+ Args :: [string()],
+ Result :: {ok, CallTX} | {error, Reason},
+ CallTX :: binary(),
+ Reason :: term().
+%% @doc
+%% Just like contract_call/5, but allows you to specify the amount of gas
+%% without getting into a major adventure with the other arguments.
+%%
+%% For details on the meaning of these and other argument values see the doc comment
+%% for contract_call/10.
+
+contract_call(CallerID, Gas, AACI, ConID, Fun, Args) ->
+ {ok, Nonce} = next_nonce(CallerID),
+ GasPrice = min_gas_price(),
+ Fee = min_fee(),
+ Amount = 0,
+ contract_call(CallerID, Nonce,
+ Gas, GasPrice, Fee, Amount,
+ AACI, ConID, Fun, Args).
-spec contract_call(CallerID, Nonce,
Gas, GasPrice, Fee, Amount,
- ACI, ConID, Fun, Args) -> CallTX
- when CallerID :: binary(),
+ AACI, ConID, Fun, Args) -> Result
+ when CallerID :: unicode:chardata(),
Nonce :: pos_integer(),
Gas :: pos_integer(),
GasPrice :: pos_integer(),
Fee :: non_neg_integer(),
- Amount :: pos_integer(),
- ACI :: binary(),
- ConID :: binary(),
+ Amount :: non_neg_integer(),
+ AACI :: map(),
+ ConID :: unicode:chardata(),
Fun :: string(),
Args :: [string()],
- CallTX :: string().
+ Result :: {ok, CallTX} | {error, Reason},
+ CallTX :: binary(),
+ Reason :: term().
%% @doc
%% Form a contract call using the supplied values.
%%
@@ -786,7 +1098,8 @@ contract_call(CallerID, Nonce, ACI, ConID, Fun, Args) ->
%% CallerID:
%% This is the public key of the entity making the contract call.
%% The key must be encoded as a binary string prefixed with <<"ak_">>.
-%% The returned call will still need to be signed by the caller's private.
+%% The returned call will still need to be signed by the caller's private
+%% key.
%%
%% -
%% Nonce:
@@ -796,8 +1109,7 @@ contract_call(CallerID, Nonce, ACI, ConID, Fun, Args) ->
%% This avoids replay attacks and ensures indempotency despite the distributed
%% nature of the blockchain network).
%% Every CallerID on the chain has a "next nonce" value that can be discovered by
-%% querying your Aeternity node (via `v_ejaa:next_nonce(CallerID, Node)', for
-%% example).
+%% querying your Aeternity node (via `vanillae:next_nonce(CallerID)', for example).
%%
%% -
%% Gas:
@@ -882,18 +1194,39 @@ contract_call(CallerID, Nonce, ACI, ConID, Fun, Args) ->
%% if you do not already have a copy, and can check the spec of a function before
%% trying to form a contract call.
-contract_call(CallerID, Nonce, Gas, GasPrice, Fee, Amount, ACI, ConID, Fun, Args) ->
- {ok, CallData} = encode_call_data(ACI, Fun, Args),
+contract_call(CallerID, Nonce, Gas, GP, Fee, Amount, AACI, ConID, Fun, Args) ->
+ case encode_call_data(AACI, Fun, Args) of
+ {ok, CD} -> contract_call2(CallerID, Nonce, Gas, GP, Fee, Amount, ConID, CD);
+ Error -> Error
+ end.
+
+contract_call2(CallerID, Nonce, Gas, GasPrice, Fee, Amount, ConID, CallData) ->
+ CallerBin = unicode:characters_to_binary(CallerID),
+ try
+ {account_pubkey, PK} = aeser_api_encoder:decode(CallerBin),
+ contract_call3(PK, Nonce, Gas, GasPrice, Fee, Amount, ConID, CallData)
+ catch
+ Error:Reason -> {Error, Reason}
+ end.
+
+contract_call3(PK, Nonce, Gas, GasPrice, Fee, Amount, ConID, CallData) ->
+ ConBin = unicode:characters_to_binary(ConID),
+ try
+ {contract_pubkey, CK} = aeser_api_encoder:decode(ConBin),
+ contract_call4(PK, Nonce, Gas, GasPrice, Fee, Amount, CK, CallData)
+ catch
+ Error:Reason -> {Error, Reason}
+ end.
+
+contract_call4(PK, Nonce, Gas, GasPrice, Fee, Amount, CK, CallData) ->
ABI = 3,
- TTL = 100,
+ TTL = 0,
CallVersion = 1,
Type = contract_call_tx,
- {account_pubkey, PK} = aeser_api_encoder:decode(CallerID),
- {contract_pubkey, CK} = aeser_api_encoder:decode(ConID),
Fields =
- [{caller_id, {id, account, PK}},
+ [{caller_id, aeser_id:create(account, PK)},
{nonce, Nonce},
- {contract_id, {id, contract, CK}},
+ {contract_id, aeser_id:create(contract, CK)},
{abi_version, ABI},
{fee, Fee},
{ttl, TTL},
@@ -913,7 +1246,11 @@ contract_call(CallerID, Nonce, Gas, GasPrice, Fee, Amount, ACI, ConID, Fun, Args
{gas_price, int},
{call_data, binary}],
TXB = aeser_chain_objects:serialize(Type, CallVersion, Template, Fields),
- aeser_api_encoder:encode(transaction, TXB).
+ try
+ {ok, aeser_api_encoder:encode(transaction, TXB)}
+ catch
+ error:Reason -> {error, Reason}
+ end.
-spec prepare_contract(File) -> {ok, AACI} | {error, Reason}
@@ -926,7 +1263,7 @@ contract_call(CallerID, Nonce, Gas, GasPrice, Fee, Amount, ACI, ConID, Fun, Args
prepare_contract(File) ->
case aeso_compiler:file(File, [{aci, json}]) of
- {ok, #{aci := ACI}} -> prepare_aaci(ACI);
+ {ok, #{aci := ACI}} -> {ok, prepare_aaci(ACI)};
Error -> Error
end.
@@ -960,21 +1297,43 @@ type(Name) -> binary_to_list(Name).
%type(#{<<"map">> := {K, V}} -> {map, type(K), type(V)};
%type(<<"string">>) -> string;
-coerce({integer, S}) ->
- list_to_integer(S);
-coerce({address, S}) ->
- {account_pubkey, Key} = aeser_api_encoder:decode(S),
- {address, Key};
-coerce({contract, S}) ->
- aeser_api_encoder:decode(S);
-coerce({bool, S}) ->
- S;
-coerce({_, S}) ->
- S.
+coerce({{ArgName, integer}, S}, {Good, Broken}) ->
+ try
+ N = list_to_integer(S),
+ {[N | Good], Broken}
+ catch
+ error:Reason -> {Good, [{ArgName, Reason} | Broken]}
+ end;
+coerce({{ArgName, address}, S}, {Good, Broken}) ->
+ try
+ case aeser_api_encoder:decode(unicode:characters_to_binary(S)) of
+ {account_pubkey, Key} -> {[{address, Key} | Good], Broken};
+ _ -> {Good, [{ArgName, bad_pubkey} | Broken]}
+ end
+ catch
+ error:Reason -> {Good, [{ArgName, Reason} | Broken]}
+ end;
+coerce({{ArgName, contract}, S}, {Good, Broken}) ->
+ try
+ case aeser_api_encoder:decode(unicode:characters_to_binary(S)) of
+ R = {contract_bytearray, _} -> {[R | Good], Broken};
+ _ -> {Good, [{ArgName, bad_contract} | Broken]}
+ end
+ catch
+ error:Reason -> {Good, [{ArgName, Reason} | Broken]}
+ end;
+coerce({{_, bool}, true}, {Good, Broken}) ->
+ {[true | Good], Broken};
+coerce({{_, bool}, false}, {Good, Broken}) ->
+ {[false | Good], Broken};
+coerce({{ArgName, bool}, _}, {Good, Broken}) ->
+ {Good, [{ArgName, not_bool} | Broken]};
+coerce({_, S}, {Good, Broken}) ->
+ {[S | Good], Broken}.
-spec min_gas_price() -> integer().
-%% @private
+%% @doc
%% This function always returns 1,000,000,000 in the current version.
%%
%% This is the minimum gas price returned by aec_tx_pool:minimum_miner_gas_price(),
@@ -990,12 +1349,95 @@ min_gas_price() ->
1000000000.
-encode_call_data({aaci, _Name, FunDefs}, Fun, Args) ->
- ArgDef = maps:get(Fun, FunDefs),
- Binding = lists:zip([element(2, D) || D <- ArgDef], Args),
- Coerced = lists:map(fun coerce/1, Binding),
- aeb_fate_abi:create_calldata(Fun, Coerced).
+-spec min_gas() -> integer().
+%% @doc
+%% This function always returns 20,000 in the current version.
+%%
+%% There is no actual minimum gas price, but this figure provides a lower limit toward
+%% successful completion of general contract calls while not too severely limiting the
+%% number of TXs that may appear in a single microblock based on the per-block gas
+%% maximum (6,000,000 / 20,000 = 300 TXs in a microblock -- which at the moment seems
+%% like plenty).
+min_gas() ->
+ 20000.
+
+
+-spec min_fee() -> integer().
+%% @doc
+%% This function always returns 200,000,000,000,000 in the current version.
+%%
+%% This is the minimum fee amount currently accepted -- it is up to callers whether
+%% they want to customize this value higher (or possibly lower, though as things stand
+%% that would only work on an independent AE-based network, not the actual Aeternity
+%% mainnet or testnet).
+
+min_fee() ->
+ 200000000000000.
+
+
+encode_call_data({aaci, _, FunDefs}, Fun, Args) ->
+ case maps:find(Fun, FunDefs) of
+ {ok, ArgDef} -> encode_call_data2(ArgDef, Fun, Args);
+ error -> {error, bad_fun_name}
+ end.
+
+encode_call_data2(ArgDef, Fun, Args) ->
+ DefLength = length(ArgDef),
+ ArgLength = length(Args),
+ if
+ DefLength =:= ArgLength -> encode_call_data3(ArgDef, Fun, Args);
+ DefLength > ArgLength -> {error, too_few_args};
+ DefLength < ArgLength -> {error, too_many_args}
+ end.
+
+encode_call_data3(ArgDef, Fun, Args) ->
+ Binding = lists:zip(ArgDef, Args),
+ case lists:foldl(fun coerce/2, {[], []}, Binding) of
+ {Coerced, []} ->
+ Reversed = lists:reverse(Coerced),
+ aeb_fate_abi:create_calldata(Fun, Reversed);
+ {_, Errors} ->
+ {error, {args, lists:reverse(Errors)}}
+ end.
+
+
+verify_signature(Sig, Message, PubKey) ->
+ case aeser_api_encoder:decode(PubKey) of
+ {account_pubkey, PK} -> verify_signature2(Sig, Message, PK);
+ Other -> {error, {bad_key, Other}}
+ end.
+
+verify_signature2(Sig, Message, PK) ->
+ Prefix = <<"aeternity Signed Message:\n">>,
+ {ok, PSize} = vencode(byte_size(Prefix)),
+ {ok, MSize} = vencode(byte_size(Message)),
+ Smashed = iolist_to_binary([PSize, Prefix, MSize, Message]),
+ {ok, Hashed} = eblake2:blake2b(32, Smashed),
+ Signature = <<(binary_to_integer(Sig, 16)):(64 * 8)>>,
+ Result = enacl:sign_verify_detached(Signature, Hashed, PK),
+ {ok, Result}.
+
+
+vencode(N) when N < 0 ->
+ {error, {negative_N, N}};
+vencode(N) when N < 16#FD ->
+ {ok, <>};
+vencode(N) when N =< 16#FFFF ->
+ NBytes = eu(N, 2),
+ {ok, <<16#FD, NBytes/binary>>};
+vencode(N) when N =< 16#FFFF_FFFF ->
+ NBytes = eu(N, 4),
+ {ok, <<16#FE, NBytes/binary>>};
+vencode(N) when N < (2 bsl 64) ->
+ NBytes = eu(N, 8),
+ {ok, <<16#FF, NBytes/binary>>}.
+
+eu(N, Size) ->
+ Bytes = binary:encode_unsigned(N, little),
+ NExtraZeros = Size - byte_size(Bytes),
+ ExtraZeros = << <<0>> || _ <- lists:seq(1, NExtraZeros) >>,
+ <>.
%%% Debug functionality
diff --git a/bindings/erlang/src/vanillae_man.erl b/bindings/erlang/src/vanillae_man.erl
index a090b35..53a6e68 100644
--- a/bindings/erlang/src/vanillae_man.erl
+++ b/bindings/erlang/src/vanillae_man.erl
@@ -37,7 +37,7 @@
-record(fetcher,
{pid = none :: none | pid(),
mon = none :: none | reference(),
- time = none :: none | erlang:timestamp(),
+ time = none :: none | integer(), % nanosecond timestamp
node = none :: none | vanilae:ae_node(),
from = none :: none | gen_server:from(),
req = none :: none | binary()}).
diff --git a/docs/baseN/README.md b/docs/baseN/README.md
index 339a58a..4d723c2 100644
--- a/docs/baseN/README.md
+++ b/docs/baseN/README.md
@@ -76,3 +76,170 @@ b64_dec([W, X, Y, Z | Rest], Acc) ->
NewAcc = <>,
b64_dec(Rest, NewAcc).
```
+
+## Introduction
+
+This document explains the Base58 and Base64 notations, and the algorithms
+for working with them. I wrote this document because I had a fair bit of
+difficulty working this out for myself, even with a strong math background.
+I couldn't find any resource on the internet explaining all of this simply.
+
+Code examples are given in Erlang and TypeScript. These are the two most
+common languages used within the Aeternity project, and both happen to be
+languges that make these tasks easy. This document assumes you are familiar
+with either/both languages. Even if that's not true, Erlang is a very simple
+and clean language, so the code should be pretty self-explanatory if you read
+it.
+
+Base64 is kind of annoying but it's pretty straightforward to code. My
+initial assumption was that Base58 was in some way "the same" algorithm but
+with `n = 58` instead of `n = 64`. When I went to look up the spec, I found
+this ([source](https://digitalbazaar.github.io/base58-spec/))
+
+> ### 3. The Base58 Encoding Algorithm
+>
+> To encode an array of bytes to a Base58 encoded value, run the following
+> algorithm. All mathematical operations MUST be performed using integer
+> arithmetic. Start by initializing a `zero_counter` to zero (`0x0`), an
+> `encoding_flag` to zero (`0x0`), a `b58_bytes` array, a `b58_encoding`
+> array, and a `carry` value to zero (`0x0`). For each byte in the array of
+> bytes and while `carry` does not equal zero (`0x0`) after the first
+> iteration:
+>
+> 1. If `encoding_flag` is not set, and if the byte is a zero (`0x0`),
+> increment the value of `zero_counter`. If the value is not zero `(0x0)`,
+> set `encoding_flag` to true `(0x1)`.
+> 2. If `encoding_flag` is set, multiply the current byte value by 256 and add
+> it to `carry`.
+> 3. Set the corresponding byte value in `b58_bytes` to the value of `carry`
+> modulus 58.
+> 4. Set `carry` to the value of `carry` divided by 58.
+>
+> Once the `b58_bytes` array has been constructed, generate the final
+> `b58_encoding` using the following algorithm. Set the first `zero_counter`
+> bytes in `b58_encoding` to `1`. Then, for every byte in `b58_array`, map the
+> byte value using the Base58 alphabet in the previous section to its
+> corresponding character in `b58_encoding`. Return `b58_encoding` as the
+> Base58 representation of the input array of bytes.
+
+I personally have no idea what that does. I found a YouTube video that
+explained the Base58 algorithm in a way that made a lot more sense.
+([source](https://youtu.be/GedV3S9X89c)). The video gave a clear enough
+explanation of the Base58 algorithm that I could _figure out_ what is going on
+and why it makes sense. I was able to relate what I was seeing in the video to
+background context I happen to have from mathematics. But the video didn't
+provide that context.
+
+I want this document to explain what both algorithms do, why they make sense,
+how they are different, and why they _have_ to be different. All with code
+examples and sufficient mathematical context.
+
+Let's get started.
+
+Any data stored in a computer is represented as an integer. For the purposes of
+this discussion, we're going to assume all integers are non-negative (greater
+than or equal to 0). The discussion below can easily be modified to accomodate
+negative integers. This would add a small amount of annoying complexity in
+exchange for no gain in conceptual clarity. Nothing we are doing requires
+dealing with negative numbers.
+
+The problem we are interested in is _how do we represent really big integers in
+plain text?_.
+
+The first point I want you to take away is that **these are two totally
+different solutions**. Do not be fooled by the name. It is **NOT** the
+case that these are two instances of the same "Base N"
+algorithm, just one is `N = 64` and one is `N = 58`. **These are two totally
+different approaches to solving the same problem.**
+
+More precisely, the underlying mathematics behind the two notations is very
+similar, but the algorithms for producing them are very different. More detail
+later.
+
+Like I said, any given piece of data is---from the perspective of your
+computer---just a very big integer. The difference between the two algorithms
+is, roughly:
+
+1. The Base64 algorithm thinks of that integer as a "stream of digits"
+2. The Base58 algorithm thinks of that integer as a "pure integer," kind of
+ the way math thinks of an integer: the integer _itself_ is a different
+ thing than the way the integer is _represented_.
+
+Base64 encoding/decoding involves a straightforward translation back and forth
+from the machine representation of integers, without thinking too much (or at
+all) about the math involved.
+
+Base58 encoding/decoding requires thinking about the integer from a more mathy
+point of view. That weird arcane algorithm above is what happens when you try
+to phrase the mathematics in terms of the machine representation of
+really big integers.
+
+We're going to focus on the mathy point of view and then circle back to the
+weird arcane algorithm later on.
+
+There is actually a good reason we don't use decimal notation for really big
+integers: it's extremely wasteful.
+
+I'll explain the following in more detail in a later section. Roll with me. To
+any piece of data there is associated a quantity called **information**. The
+_unit_ of information is the _bit_, in the same sense that the unit of length
+is the meter.
+
+1. There are 256 distinct bytes. A single byte (machine digit)
+ contains exactly 8 bits $8 = \log_2 256$ of information.
+
+2. There are 10 distinct decimal ("Base10") symbols. A single decimal digit
+ contains approximately 3.32 bits (`3.32 ~ log2(10)`) of information.
+
+3. There are 64 distinct Base64 symbols. A single Base64 digit contains
+ exactly $6$ bits (`6 = log2(64)`) of information.
+
+4. There are 58 distinct Base58 symbols. A single Base58 digit contains
+ approximately 5.86 bits (`5.86 ~ log2(58)`) of information.
+
+What this means is, in base64 notation, each symbol consumes 6 bits of
+information, roughly twice the rate of decimal notation (~3.32 bits per
+symbol). What this means in practice is that a number written in Base64
+notation is about half as long as a number written in decimal notation.
+
+For instance, the number `K = 90 682 877 680 429`
+
+1. requires 14 digits (count them!) in decimal notation
+
+ $$
+ \frac{(\log_2 K) \text{ bits}}
+ {(\log_2 10) \text{ bits per symbol}}
+ \approx
+ \frac{46.37 \text{ bits}}
+ { 3.37 \text{ bits per symbol}}
+ \approx 13.96 \text{ symbols}
+ $$
+
+2. requires 8 digits in Base64 notation (`UnnAthst`)
+
+ $$
+ \frac{(\log_2 K) \text{ bits}}
+ {(\log_2 64) \text{ bits per symbol}}
+ \approx
+ \frac{46.37 \text{ bits}}
+ { 6 \text{ bits per symbol}}
+ \approx 7.73 \text{ symbols}
+ $$
+
+3. requires 8 digits in Base58 notation (`i55xNZNt`)
+
+ $$
+ \frac{(\log_2 K) \text{ bits}}
+ {(\log_2 58) \text{ bits per symbol}}
+ \approx
+ \frac{46.37 \text{ bits}}
+ { 5.86 \text{ bits per symbol}}
+ \approx 7.91 \text{ symbols}
+ $$
+
+As you can see, the difference in space complexity between Base58 and Base64 is
+pretty small, but the difference between Base10 is pretty large. Base58 has
+the same alphabet (set of symbols) as Base64, minus a handful that can cause
+readability or manual input issues. For instance, the Base64 alphabet contains
+both the symbol `0` (numeral zero) and `O` (uppercase letter `o`). The Base58
+alphabet contains neither.