hb_ao_device.erl - Device Management Library
Overview
Purpose: Device loading, verification, and function extraction
Module: hb_ao_device
Pattern: Device abstraction layer
Security: Trusted signer verification for remote devices
This module provides services for working with HyperBEAM-compatible AO-Core devices, including loading devices from Arweave, verifying compatibility, and extracting Erlang functions.
Dependencies
- Erlang/OTP:
erlang - HyperBEAM:
hb_ao,hb_maps,hb_util,hb_message,hb_cache,hb_store,hb_opts,dev_message - Includes:
include/hb.hrl
Public Functions Overview
%% Device Loading
-spec load(DeviceID, Opts) -> {ok, Module} | {error, Reason}.
%% Function Extraction
-spec message_to_fun(Msg, Key, Opts) -> {Status, Device, Function}.
-spec message_to_device(Msg, Opts) -> Device.
-spec find_exported_function(Msg, Dev, Key, MaxArity, Opts) -> {ok, Fun} | not_found.
%% Device Information
-spec info(DeviceModule, Msg, Opts) -> InfoMap.
-spec info(Msg, Opts) -> InfoMap.
-spec default() -> DefaultDevice.
%% Validation
-spec is_exported(Msg, Dev, Key, Opts) -> boolean().
-spec is_exported(Info, Key, Opts) -> boolean().
-spec is_direct_key_access(Base, Req, Opts) -> boolean().
-spec is_direct_key_access(Base, Req, Opts, Store) -> boolean().
%% Utilities
-spec truncate_args(Fun, Args) -> TruncatedArgs.Device Resolution Forms
1. Default Device
% No device specified → use dev_message
2. Handler Function
% Device has handler in info() → use it
3. Exported Function
% Device exports function named Key → call it
4. Default Handler
% Device has default in info() → use it
5. Default Device Fallback
% No handler → use dev_message
Error: Device specified but not loadablePublic Functions
1. load/2
-spec load(DeviceID, Opts) -> {ok, Module} | {error, Reason}
when
DeviceID :: binary() | atom(),
Opts :: map(),
Module :: atom(),
Reason :: term().Description: Load a device module. Can load from:
- Atom: Preloaded device name
- Binary: Arweave TX ID (if
load_remote_devicesenabled) - Map: Device definition
Security: Remote devices must be signed by trusted signers.
Test Code:-module(hb_ao_device_load_test).
-include_lib("eunit/include/eunit.hrl").
-include("include/hb.hrl").
load_preloaded_test() ->
Opts = #{
preloaded_devices => [
#{
<<"name">> => <<"test@1.0">>,
<<"module">> => dev_message
}
]
},
{ok, Mod} = hb_ao_device:load(<<"test@1.0">>, Opts),
?assertEqual(dev_message, Mod).
load_atom_test() ->
{ok, Mod} = hb_ao_device:load(dev_message, #{}),
?assertEqual(dev_message, Mod).
load_not_found_test() ->
Result = hb_ao_device:load(<<"nonexistent@1.0">>, #{}),
?assertMatch({error, _}, Result).
load_default_device_test() ->
{ok, Mod} = hb_ao_device:load(<<"message@1.0">>, #{}),
?assertEqual(dev_message, Mod).2. message_to_fun/3
-spec message_to_fun(Msg, Key, Opts) -> {Status, Device, Function}
when
Msg :: map(),
Key :: binary(),
Opts :: map(),
Status :: ok | add_key,
Device :: atom(),
Function :: function().Description: Extract the Erlang function that should be called for a given key.
Returns:{ok, Device, Fun}- Function ready to call{add_key, Device, Fun}- Add key as first argument
-module(hb_ao_device_message_to_fun_test).
-include_lib("eunit/include/eunit.hrl").
-include("include/hb.hrl").
message_to_fun_default_device_test() ->
Msg = #{<<"data">> => <<"value">>},
{_Status, Dev, Fun} = hb_ao_device:message_to_fun(Msg, <<"data">>, #{}),
?assertEqual(dev_message, Dev),
?assert(is_function(Fun)).
message_to_fun_exported_test() ->
Msg = #{<<"device">> => dev_message},
{ok, Dev, Fun} = hb_ao_device:message_to_fun(Msg, <<"get">>, #{}),
?assertEqual(dev_message, Dev),
?assert(is_function(Fun)).
message_to_fun_unknown_key_test() ->
Msg = #{<<"device">> => dev_message},
{_Status, Dev, Fun} = hb_ao_device:message_to_fun(Msg, <<"unknown_key">>, #{}),
?assertEqual(dev_message, Dev),
?assert(is_function(Fun)).3. message_to_device/2
-spec message_to_device(Msg, Opts) -> Device
when
Msg :: map(),
Opts :: map(),
Device :: atom() | map().Description: Extract device module from message. Returns default if not specified.
Test Code:-module(hb_ao_device_message_to_device_test).
-include_lib("eunit/include/eunit.hrl").
-include("include/hb.hrl").
message_to_device_default_test() ->
Msg = #{<<"data">> => <<"value">>},
Dev = hb_ao_device:message_to_device(Msg, #{}),
?assertEqual(dev_message, Dev).
message_to_device_specified_test() ->
Msg = #{<<"device">> => <<"custom@1.0">>},
Opts = #{
preloaded_devices => [
#{<<"name">> => <<"custom@1.0">>, <<"module">> => dev_message}
]
},
Dev = hb_ao_device:message_to_device(Msg, Opts),
?assertEqual(dev_message, Dev).4. find_exported_function/5
-spec find_exported_function(Msg, Dev, Key, MaxArity, Opts) ->
{ok, Function} | not_found
when
Msg :: map(),
Dev :: atom() | map(),
Key :: binary() | atom(),
MaxArity :: integer(),
Opts :: map(),
Function :: function().Description: Find function with highest arity ≤ MaxArity that matches the key name.
Test Code:-module(hb_ao_device_find_exported_test).
-include_lib("eunit/include/eunit.hrl").
-include("include/hb.hrl").
find_exported_function_module_test() ->
Msg = #{},
{ok, Fun} = hb_ao_device:find_exported_function(
Msg, dev_message, get, 3, #{}
),
?assert(is_function(Fun)),
{arity, Arity} = erlang:fun_info(Fun, arity),
?assert(Arity =< 3).
find_exported_function_map_test() ->
Dev = #{
<<"my_key">> => fun(M1, _M2) -> {ok, M1} end
},
{ok, Fun} = hb_ao_device:find_exported_function(
#{}, Dev, <<"my_key">>, 3, #{}
),
?assert(is_function(Fun)).
find_exported_function_not_found_test() ->
Result = hb_ao_device:find_exported_function(
#{}, dev_message, <<"nonexistent">>, 3, #{}
),
?assertEqual(not_found, Result).5. info/2, info/3
-spec info(Msg, Opts) -> InfoMap.
-spec info(DeviceModule, Msg, Opts) -> InfoMap
when
InfoMap :: map().Description: Get device info map. Calls DevMod:info/0, info/1, or info/2 if available.
exports- List of exported keysexcludes- List of excluded keyshandler- Override handler functiondefault- Default handler for unknown keysgrouper- Concurrency grouping functionworker- Server loop function
-module(hb_ao_device_info_test).
-include_lib("eunit/include/eunit.hrl").
-include("include/hb.hrl").
info_basic_test() ->
Msg = #{<<"device">> => dev_message},
Info = hb_ao_device:info(Msg, #{}),
?assert(is_map(Info)).
info_from_module_test() ->
Info = hb_ao_device:info(dev_message, #{}, #{}),
?assert(is_map(Info)).
info_no_function_test() ->
Info = hb_ao_device:info(erlang, #{}, #{}),
?assertEqual(#{}, Info).6. is_exported/4
-spec is_exported(Msg, Dev, Key, Opts) -> boolean().Description: Check if key is exported by device. Respects exports and excludes lists from device info.
-module(hb_ao_device_is_exported_test).
-include_lib("eunit/include/eunit.hrl").
-include("include/hb.hrl").
is_exported_true_test() ->
Msg = #{},
?assertEqual(true, hb_ao_device:is_exported(Msg, dev_message, <<"get">>, #{})).
is_exported_false_test() ->
Msg = #{},
?assertEqual(false, hb_ao_device:is_exported(Msg, dev_message, <<"nonexistent_key_xyz">>, #{})).
is_exported_info_always_true_test() ->
Msg = #{},
?assertEqual(true, hb_ao_device:is_exported(Msg, dev_message, info, #{})).7. is_direct_key_access/3, is_direct_key_access/4
-spec is_direct_key_access(Base, Req, Opts) -> boolean() | unknown.
-spec is_direct_key_access(Base, Req, Opts, Store) -> boolean() | unknown.Description: Check if key access is "direct" - literal key in map will be returned without device processing.
Test Code:-module(hb_ao_device_is_direct_test).
-include_lib("eunit/include/eunit.hrl").
-include("include/hb.hrl").
is_direct_key_access_message_device_test() ->
Base = #{
<<"device">> => <<"message@1.0">>,
<<"data">> => <<"value">>
},
Req = #{<<"path">> => <<"data">>},
% data is direct access in message@1.0
?assertEqual(true, hb_ao_device:is_direct_key_access(Base, Req, #{})).
is_direct_key_access_message_keys_test() ->
Base = #{<<"device">> => <<"message@1.0">>},
% get/set/remove are not direct access
?assertEqual(false,
hb_ao_device:is_direct_key_access(Base, #{<<"path">> => <<"get">>}, #{})),
?assertEqual(false,
hb_ao_device:is_direct_key_access(Base, #{<<"path">> => <<"set">>}, #{})).
is_direct_key_access_no_device_test() ->
% Without device, uses default (message@1.0)
Base = #{<<"data">> => <<"value">>},
Req = #{<<"path">> => <<"data">>},
Result = hb_ao_device:is_direct_key_access(Base, Req, #{}),
?assertEqual(true, Result).8. truncate_args/2
-spec truncate_args(Fun, Args) -> TruncatedArgs
when
Fun :: function(),
Args :: list(),
TruncatedArgs :: list().Description: Truncate argument list to match function arity.
Test Code:-module(hb_ao_device_truncate_test).
-include_lib("eunit/include/eunit.hrl").
-include("include/hb.hrl").
truncate_args_test() ->
Fun = fun(A, B) -> {A, B} end,
Args = [1, 2, 3, 4, 5],
Truncated = hb_ao_device:truncate_args(Fun, Args),
?assertEqual([1, 2], Truncated).
truncate_args_exact_test() ->
Fun = fun(A, B, C) -> {A, B, C} end,
Args = [1, 2, 3],
Truncated = hb_ao_device:truncate_args(Fun, Args),
?assertEqual([1, 2, 3], Truncated).
truncate_args_less_test() ->
Fun = fun(A, B, C) -> {A, B, C} end,
Args = [1, 2],
Truncated = hb_ao_device:truncate_args(Fun, Args),
?assertEqual([1, 2], Truncated).9. default/0
-spec default() -> DefaultDevice
when DefaultDevice :: atom().Description: Return the default device (dev_message).
-module(hb_ao_device_default_test).
-include_lib("eunit/include/eunit.hrl").
-include("include/hb.hrl").
default_test() ->
?assertEqual(dev_message, hb_ao_device:default()).Device Info Structure
#{
%% Export control
exports => [key1, key2, ...], % Only these keys exported
excludes => [internal_key, ...], % Exclude from exports
%% Handlers
handler => Function, % Override all key handling
default => DefaultFunction | Mod, % Default for unknown keys
%% Concurrency
grouper => fun(M1, M2) -> Group end, % Execution grouping
worker => fun() -> loop() end, % Server loop
%% Other
custom_field => Value % Device-specific data
}Remote Device Loading
Requirements
% Must be enabled
load_remote_devices => true
% Device must be signed by trusted signer
trusted_device_signers => [<<"signer_address1">>, <<"signer_address2">>]
% Device message must have
#{
<<"content-type">> => <<"application/beam">>,
<<"module-name">> => <<"device_name">>,
<<"body">> => <<BEAM binary>>,
<<"requires-", Key/binary>> => SystemValue % Optional compatibility
}Compatibility Verification
% Device can specify system requirements
#{
<<"requires-otp_release">> => <<"27">>,
<<"requires-machine">> => <<"BEAM">>,
<<"requires-wordsize">> => <<"8">>
}
% System must match
erlang:system_info(otp_release) == <<"27">>
erlang:system_info(machine) == <<"BEAM">>
erlang:system_info(wordsize) == 8Common Patterns
%% Load preloaded device
{ok, DevMod} = hb_ao_device:load(<<"my_device@1.0">>, Opts).
%% Get function for key
{ok, Dev, Fun} = hb_ao_device:message_to_fun(Msg, <<"key">>, Opts),
Result = apply(Fun, [Msg1, Msg2, Opts]).
%% Check if key is direct access
case hb_ao_device:is_direct_key_access(Base, Req, Opts) of
true -> maps:get(Key, Base);
false -> resolve_via_device(Base, Req, Opts)
end.
%% Get device info
Info = hb_ao_device:info(dev_message, #{}, #{}),
Exports = maps:get(exports, Info, []).
%% Check if exported
case hb_ao_device:is_exported(Msg, Dev, Key, Opts) of
true -> call_function(Dev, Key);
false -> use_default_handler()
end.References
- Device System - Core AO-Core device architecture
- hb_ao - Resolution engine
- dev_message - Default device implementation
- Arweave - Remote device storage
Notes
- Default Device: Always
dev_message - Security: Remote devices require trusted signers
- BEAM Format: Remote devices must be compiled BEAM files
- Compatibility: Devices can specify system requirements
- Arity Matching: Functions matched by name and arity
- Info Function: Optional, returns empty map if missing
- Handler Priority: Handler > Exported > Default > dev_message
- Direct Access: Optimization for literal key reads
- Excludes: Keys in excludes list not exported even if in exports
- Argument Truncation: Functions called with correct arity