hb.erl - HyperBEAM Core Module
Overview
Purpose: Core configuration, initialization, and utility functions for HyperBEAM
Module: hb
Role: Primary entry point for node configuration, wallet management, and debugging
This module provides the foundational utilities for the HyperBEAM node, including system initialization, node startup configurations, wallet management, and debugging tools. It serves as the main interface for configuring and operating a HyperBEAM node.
Dependencies
- Erlang/OTP:
application,file,erlang,rand,filelib - HyperBEAM:
hb_opts,hb_name,hb_http_server,hb_http,hb_cache,hb_store,hb_message,hb_client,hb_util,hb_maps - Arweave:
ar_wallet - External:
r3(rebar3 interface)
Public Functions Overview
%% Initialization
-spec init() -> ok.
-spec now() -> integer().
-spec build() -> term().
-spec deploy_scripts() -> ok.
%% Node Startup
-spec start_mainnet() -> binary().
-spec start_mainnet(Port | Opts) -> binary().
-spec start_simple_pay() -> binary().
-spec start_simple_pay(Addr) -> binary().
-spec start_simple_pay(Addr, Port) -> binary().
-spec topup(Node, Amount, Recipient) -> term().
-spec topup(Node, Amount, Recipient, Wallet) -> term().
%% Wallet Management
-spec wallet() -> ar_wallet:wallet().
-spec wallet(Location) -> ar_wallet:wallet().
-spec address() -> binary().
%% Debugging
-spec read(ID) -> term().
-spec read(ID, Scope | Store) -> term().
-spec no_prod(X, Mod, Line) -> X | no_return().
-spec debug_wait(T, Mod, Func, Line) -> ok.Public Functions
1. init/0
-spec init() -> ok.Description: Initialize system-wide settings for the HyperBEAM node. Starts the naming service and sets the Erlang backtrace depth for debugging.
Side Effects:- Starts
hb_nameservice - Sets
backtrace_depthsystem flag from config
-module(hb_init_test).
-include_lib("eunit/include/eunit.hrl").
init_test() ->
% init/0 just sets backtrace depth and starts hb_name
?assertEqual(ok, hb:init()),
% Verify backtrace depth was set
Depth = erlang:system_flag(backtrace_depth, 20),
?assert(is_integer(Depth)).2. now/0
-spec now() -> integer().Description: Get the current time in milliseconds since epoch. Wrapper around erlang:system_time(millisecond).
-module(hb_now_test).
-include_lib("eunit/include/eunit.hrl").
now_test() ->
% now/0 just returns erlang:system_time(millisecond)
Time = hb:now(),
?assert(is_integer(Time)),
?assert(Time > 0),
% Verify time increases
T1 = hb:now(),
timer:sleep(10),
T2 = hb:now(),
?assert(T2 >= T1).3. build/0
-spec build() -> term().Description: Hot-recompile and load the HyperBEAM environment using rebar3. Useful during development for live code reloading.
Note: This function requires rebar3 to be available and is primarily for development use.
Test Code:-module(hb_build_test).
-include_lib("eunit/include/eunit.hrl").
build_test() ->
% build/0 requires r3 module (rebar3 shell only)
% Just verify function is exported
?assert(erlang:function_exported(hb, build, 0)).4. wallet/0, wallet/1
-spec wallet() -> ar_wallet:wallet().
-spec wallet(Location) -> ar_wallet:wallet()
when Location :: string() | binary().Description: Load or create a wallet from disk. If the wallet file doesn't exist, creates a new keyfile at the specified location.
Behavior:- 0-arity: Uses
priv_key_locationfrom config - 1-arity: Uses specified location
Note: Internally uses wallet/2 which accepts additional options, but this is not exported.
-module(hb_wallet_test).
-include_lib("eunit/include/eunit.hrl").
wallet_test() ->
% wallet/1 creates or loads wallet from file
TempPath = "/tmp/test_wallet_" ++ integer_to_list(erlang:unique_integer([positive])) ++ ".json",
Wallet = hb:wallet(TempPath),
?assert(is_tuple(Wallet)),
file:delete(TempPath).
wallet_creates_new_test() ->
TempPath = "/tmp/test_wallet_new_" ++ integer_to_list(erlang:unique_integer([positive])) ++ ".json",
file:delete(TempPath),
?assertEqual({error, enoent}, file:read_file_info(TempPath)),
Wallet = hb:wallet(TempPath),
?assert(is_tuple(Wallet)),
?assertMatch({ok, _}, file:read_file_info(TempPath)),
file:delete(TempPath).
wallet_loads_existing_test() ->
TempPath = "/tmp/test_wallet_exist_" ++ integer_to_list(erlang:unique_integer([positive])) ++ ".json",
Wallet1 = hb:wallet(TempPath),
Wallet2 = hb:wallet(TempPath),
?assertEqual(Wallet1, Wallet2),
file:delete(TempPath).5. address/0
-spec address() -> binary().Description: Get the base64url-encoded address of the default wallet (from priv_key_location config).
Note: Internally uses address/1 which accepts a wallet tuple or location, but this is not exported.
-module(hb_address_test).
-include_lib("eunit/include/eunit.hrl").
address_test() ->
% Test address via wallet - doesn't need mainnet
TempPath = "/tmp/test_addr_" ++ integer_to_list(erlang:unique_integer([positive])) ++ ".json",
Wallet = hb:wallet(TempPath),
% Get address using ar_wallet (same as internal implementation)
Addr = hb_util:encode(ar_wallet:to_address(Wallet)),
?assert(is_binary(Addr)),
?assertEqual(43, byte_size(Addr)),
file:delete(TempPath).6. read/1, read/2
-spec read(ID) -> term().
-spec read(ID, Scope | Store) -> term()
when ID :: binary() | string(),
Scope :: local | remote,
Store :: term().Description: Read a message from the cache. Used for debugging to inspect cached messages.
Parameters:ID: Message identifierScope:localorremoteatom to specify store scopeStore: Direct store tuple reference
-module(hb_read_test).
-include_lib("eunit/include/eunit.hrl").
read_integration_test() ->
% Start mainnet server
Port = 10000 + rand:uniform(50000),
TempWallet = "/tmp/test_read_wallet_" ++ integer_to_list(Port) ++ ".json",
URL = hb:start_mainnet(#{
port => Port,
priv_key_location => TempWallet
}),
timer:sleep(100),
% Verify server is running
?assert(is_binary(URL)),
% Create and sign a message
Wallet = hb:wallet(TempWallet),
Msg = hb_message:commit(#{
<<"type">> => <<"test">>,
<<"data">> => <<"hello world">>
}, Wallet),
% Get the message ID
MsgID = hb_message:id(Msg),
?assert(is_binary(MsgID)),
% Post message using hb_http
{ok, _Response} = hb_http:post(URL, Msg, #{}),
% Verify via HTTP GET
GetURL = <<URL/binary, "/", (hb_util:encode(MsgID))/binary>>,
{ok, {{_, GetStatus, _}, _, _}} = httpc:request(
get,
{binary_to_list(GetURL), []},
[{timeout, 5000}],
[]
),
?assert(is_integer(GetStatus)),
% Cleanup
file:delete(TempWallet).7. no_prod/3
-spec no_prod(X, Mod, Line) -> X | no_return()
when X :: term(),
Mod :: atom(),
Line :: integer().Description: Safety function to prevent non-production-ready code from running in production mode. If mode is prod, either exits or throws an error depending on exit_on_no_prod config.
- In
prodmode withexit_on_no_prod=true: Callsinit:stop() - In
prodmode withexit_on_no_prod=false: Throws the valueX - In other modes: Returns
Xunchanged
-module(hb_no_prod_test).
-include_lib("eunit/include/eunit.hrl").
no_prod_test() ->
% no_prod/3 just checks mode - doesn't need mainnet
% In dev mode, returns value unchanged
Value = {some, test, value},
?assertEqual(Value, hb:no_prod(Value, ?MODULE, ?LINE)),
% Test with different types
?assertEqual(42, hb:no_prod(42, ?MODULE, ?LINE)),
?assertEqual(<<"binary">>, hb:no_prod(<<"binary">>, ?MODULE, ?LINE)),
?assertEqual([1,2,3], hb:no_prod([1,2,3], ?MODULE, ?LINE)).8. debug_wait/4
-spec debug_wait(T, Mod, Func, Line) -> ok
when T :: non_neg_integer(),
Mod :: atom(),
Func :: atom(),
Line :: integer().Description: Wait for a specified time while logging debug information. Useful for debugging timing-sensitive code.
Parameters:T: Time to wait in millisecondsMod: Calling module nameFunc: Calling function nameLine: Line number of call
-module(hb_debug_wait_test).
-include_lib("eunit/include/eunit.hrl").
debug_wait_test() ->
% debug_wait/4 just sleeps - doesn't need mainnet
?assertEqual(ok, hb:debug_wait(10, ?MODULE, debug_wait_test, ?LINE)),
% Verify it actually waits
T1 = hb:now(),
hb:debug_wait(50, ?MODULE, debug_wait_test, ?LINE),
T2 = hb:now(),
?assert(T2 - T1 >= 50).9. start_mainnet/0, start_mainnet/1
-spec start_mainnet() -> binary().
-spec start_mainnet(Port | Opts) -> binary()
when Port :: integer(),
Opts :: map().Description: Start a mainnet server without payments. Initializes all required OTP applications and starts the HTTP server with filesystem-based storage.
Return Value: URL binary like <<"http://localhost:8080">>
- kernel, stdlib, inets, ssl, ranch, cowboy, gun, os_mon
#{<<"store-module">> => hb_store_fs, <<"name">> => <<"cache-mainnet">>}Note: Integration tests start actual server instances on random ports.
Test Code:-module(hb_start_mainnet_test).
-include_lib("eunit/include/eunit.hrl").
start_mainnet_integration_test() ->
% Start on random port to avoid conflicts
Port = 10000 + rand:uniform(50000),
TempWallet = "/tmp/test_mainnet_wallet_" ++ integer_to_list(Port) ++ ".json",
URL = hb:start_mainnet(#{
port => Port,
priv_key_location => TempWallet
}),
?assert(is_binary(URL)),
?assertMatch(<<"http://localhost:", _/binary>>, URL),
% Verify server is responding
timer:sleep(100),
{ok, {{_, StatusCode, _}, _, _}} = httpc:request(
get,
{binary_to_list(URL), []},
[{timeout, 5000}],
[]
),
% Any response (even 404) means server is running
?assert(is_integer(StatusCode)),
% Cleanup
file:delete(TempWallet).10. start_simple_pay/0, start_simple_pay/1, start_simple_pay/2
-spec start_simple_pay() -> binary().
-spec start_simple_pay(Addr) -> binary()
when Addr :: binary().
-spec start_simple_pay(Addr, Port) -> binary()
when Addr :: binary(),
Port :: integer().Description: Start a server with simple-pay@1.0 pre-processor for payment handling. Uses P4 device for request/response processing.
#{
<<"device">> => <<"p4@1.0">>,
<<"ledger-device">> => <<"simple-pay@1.0">>,
<<"pricing-device">> => <<"simple-pay@1.0">>
}-module(hb_start_simple_pay_test).
-include_lib("eunit/include/eunit.hrl").
start_simple_pay_integration_test() ->
% Start on random port
Port = 10000 + rand:uniform(50000),
TempWallet = "/tmp/test_simplepay_wallet_" ++ integer_to_list(Port) ++ ".json",
% Create wallet - we'll use a placeholder address since address/1 is not exported
_Wallet = hb:wallet(TempWallet),
% Use a simple test address (operator address is mainly for display)
Addr = <<"test_operator_address">>,
URL = hb:start_simple_pay(Addr, Port),
?assert(is_binary(URL)),
?assertMatch(<<"http://localhost:", _/binary>>, URL),
% Verify server is responding
timer:sleep(100),
{ok, {{_, StatusCode, _}, _, _}} = httpc:request(
get,
{binary_to_list(URL), []},
[{timeout, 5000}],
[]
),
?assert(is_integer(StatusCode)),
% Cleanup
file:delete(TempWallet).11. topup/3, topup/4
-spec topup(Node, Amount, Recipient) -> term().
-spec topup(Node, Amount, Recipient, Wallet) -> term()
when Node :: binary(),
Amount :: integer(),
Recipient :: binary(),
Wallet :: ar_wallet:wallet().Description: Helper for topping up a user's balance on a simple-pay node. Sends a signed message to the node's /~simple-pay@1.0/topup endpoint.
#{
<<"path">> => <<"/~simple-pay@1.0/topup">>,
<<"amount">> => Amount,
<<"recipient">> => Recipient
}-module(hb_topup_test).
-include_lib("eunit/include/eunit.hrl").
topup_integration_test() ->
% Start a simple-pay server
Port = 10000 + rand:uniform(50000),
TempWallet = "/tmp/test_topup_wallet_" ++ integer_to_list(Port) ++ ".json",
% Create operator wallet and get address
OperatorWallet = hb:wallet(TempWallet),
OperatorAddr = hb_util:human_id(ar_wallet:to_address(OperatorWallet)),
% Start simple-pay with operator address - operator must match wallet
URL = hb:start_simple_pay(OperatorAddr, Port),
timer:sleep(100),
% Create recipient wallet
RecipientWalletPath = "/tmp/test_topup_recipient_" ++ integer_to_list(Port) ++ ".json",
RecipientWallet = hb:wallet(RecipientWalletPath),
RecipientAddr = hb_util:human_id(ar_wallet:to_address(RecipientWallet)),
% Top up the recipient - operator can topup for free
TopupAmount = 5000,
{ok, NewBalance} = hb:topup(URL, TopupAmount, RecipientAddr, OperatorWallet),
% Verify the new balance equals topup amount
?assertEqual(TopupAmount, NewBalance),
% Cleanup
file:delete(TempWallet),
file:delete(RecipientWalletPath).12. deploy_scripts/0
-spec deploy_scripts() -> ok.Description: Upload all Lua scripts from the scripts/ directory to Arweave. Prints the ID of each uploaded script.
#{
<<"data-protocol">> => <<"ao">>,
<<"variant">> => <<"ao.N.1">>,
<<"type">> => <<"module">>,
<<"content-type">> => <<"application/lua">>,
<<"name">> => FileName,
<<"body">> => ScriptContent
}-module(hb_deploy_scripts_test).
-include_lib("eunit/include/eunit.hrl").
deploy_scripts_test() ->
% deploy_scripts/0 reads .lua files from scripts/ directory
LuaFiles = filelib:wildcard("scripts/*.lua"),
case LuaFiles of
[] ->
% No lua files - returns ok immediately (no network needed)
?assertEqual(ok, hb:deploy_scripts());
Files ->
% Has lua files - start mainnet for wallet, attempt deploy
Port = 10000 + rand:uniform(50000),
TempWallet = "/tmp/test_deploy_wallet_" ++ integer_to_list(Port) ++ ".json",
_URL = hb:start_mainnet(#{
port => Port,
priv_key_location => TempWallet
}),
timer:sleep(100),
% deploy_scripts returns ok after processing all files
% (upload status is printed but doesn't affect return value)
?assertEqual(ok, hb:deploy_scripts()),
% Verify it found the expected files
?assert(length(Files) > 0),
file:delete(TempWallet)
end.Configuration Options
The hb module uses hb_opts:get/1 and hb_opts:get/3 for configuration:
| Option | Description | Default |
|---|---|---|
debug_stack_depth | Erlang backtrace depth | 20 |
priv_key_location | Path to wallet keyfile | - |
port | HTTP server port | 8080 |
mode | Operating mode (dev, prod) | dev |
exit_on_no_prod | Exit on no_prod in prod mode | false |
store | Store configuration | - |
address | Node operator address | - |
Common Patterns
%% Initialize the node
hb:init().
%% Get current timestamp
Timestamp = hb:now().
%% Load default wallet
Wallet = hb:wallet().
%% Get wallet address
Address = hb:address().
%% Load wallet from specific location
Wallet = hb:wallet("/path/to/wallet.json").
%% Read cached message
{ok, Msg} = hb:read(<<"message_id">>).
%% Read from specific scope
{ok, Msg} = hb:read(<<"message_id">>, remote).
%% Start mainnet node on default port
URL = hb:start_mainnet().
%% Start mainnet node on specific port
URL = hb:start_mainnet(9000).
%% Start simple-pay node
URL = hb:start_simple_pay().
%% Top up user balance
hb:topup(<<"http://localhost:8080">>, 1000, UserAddress).
%% Hot reload during development
hb:build().
%% Debug wait with logging
hb:debug_wait(1000, ?MODULE, my_function, ?LINE).
%% Safety check for non-prod code
Value = hb:no_prod(dangerous_value, ?MODULE, ?LINE).Architecture
Initialization Flow
hb:init()
│
├─> hb_name:start()
│ └─> Start naming/registry service
│
└─> erlang:system_flag(backtrace_depth, N)
└─> Configure debug stack tracesServer Startup Flow
hb:start_mainnet(Opts)
│
├─> application:ensure_all_started([...])
│ └─> Start OTP dependencies
│
├─> hb:wallet(Location)
│ └─> Load or create wallet
│
├─> hb_http_server:set_default_opts(Opts)
│ └─> Configure server options
│
└─> hb_http_server:start_node(FinalOpts)
└─> Start HTTP serverWallet Management
Key Types
The module uses ar_wallet for cryptographic operations:
% Default key type from hb.hrl
?DEFAULT_KEY_TYPE % Typically {rsa, 65537} or {ecdsa, secp256k1}Wallet Tuple Structure
% RSA wallet
{{rsa, 65537}, {PublicKey, PrivateKey}}
% ECDSA wallet
{{ecdsa, secp256k1}, {PublicKey, PrivateKey}}Address Encoding
Addresses are base64url-encoded SHA-256 hashes of the public key:
- 43 characters for standard addresses
- URL-safe encoding (no padding)
Debugging
Stack Trace Configuration
% Set via hb_opts
hb_opts:get(debug_stack_depth) % Default: 20
% Applied during init
erlang:system_flag(backtrace_depth, Depth)Debug Wait Usage
% In code requiring timing investigation
hb:debug_wait(5000, ?MODULE, problematic_function, ?LINE).
% Logs: {debug_wait, {5000, module, problematic_function, 42}}
% Then waits 5 secondsProduction Safety
% Mark non-production-ready code
Result = ?NO_PROD(experimental_feature()).
% Expands to: hb:no_prod(experimental_feature(), ?MODULE, ?LINE)References
- hb_opts - Configuration management
- hb_http_server - HTTP server implementation
- hb_cache - Message caching layer
- hb_store - Persistent storage abstraction
- ar_wallet - Arweave wallet operations
- hb_name - Process naming service
Notes
- Initialization Order:
hb:init/0must be called before most operations - Wallet Auto-Creation: Missing wallet files are automatically created
- Mode Safety:
no_prod/3prevents accidental prod deployment of dev code - Hot Reload:
build/0enables live code updates during development - Timestamp Precision:
now/0returns millisecond-precision timestamps - Address Format: 43-character base64url-encoded strings
- Store Scopes:
localandremoteprovide different cache views - Payment Integration:
simple-payprovides basic payment processing - Script Deployment: Lua scripts uploaded with AO protocol metadata
- Port Randomization:
start_simple_pay/0uses random port (10000-60000)