Skip to content

hb_message.erl - Message Format Conversion & Manipulation

Overview

Purpose: Message format conversion and manipulation adapter
Module: hb_message
Core Format: TABM (Type Annotated Binary Messages)
Pattern: Any format → TABM → Any format

This module acts as an adapter between different message formats in HyperBEAM, providing conversion between AO-Core structured messages, Arweave transactions, ANS-104 data items, HTTP Signed Messages, and flat maps. Unless implementing a new codec, use hb_ao interfaces instead of this module directly.

Supported Formats

  • Structured Messages: Richly typed AO-Core messages (structured@1.0)
  • TABM: Type Annotated Binary Messages (internal format)
  • ANS-104: Arweave data items (ans104@1.0)
  • HTTP Signed: HTTP signature messages (httpsig@1.0)
  • Flat Maps: Simple key-value maps (flat@1.0)

TABM Format

Definition: Deep Erlang maps containing only binaries or other TABMs

Benefits:
  • Simple computational model (O(1) map access)
  • Binary literals only (no types)
  • Easy format conversion
  • Efficient operations

Conversion Flow

Input Formats → TABM → Output Formats
 
Arweave TX/ANS-104 → dev_codec_ans104:from → TABM
HTTP Signed → dev_codec_httpsig:from → TABM
Flat Maps → dev_codec_flat:from → TABM
Structured → dev_codec_structured:from → TABM
 
TABM → dev_codec_ans104:to → Arweave TX/ANS-104
TABM → dev_codec_httpsig:to → HTTP Signed
TABM → dev_codec_structured:to → Structured
TABM → dev_codec_flat:to → Flat Maps

Dependencies

  • HyperBEAM: hb_ao, hb_util, hb_maps, hb_opts, hb_cache, hb_private
  • Codecs: dev_codec_* modules
  • Arweave: ar_wallet, ar_bundles
  • Includes: include/hb.hrl

Public Functions Overview

%% Message ID
-spec id(Msg) -> ID.
-spec id(Msg, Committers) -> ID.
-spec id(Msg, Committers, Opts) -> ID.
 
%% Format Conversion
-spec convert(Msg, TargetFormat, Opts) -> ConvertedMsg.
-spec convert(Msg, TargetFormat, SourceFormat, Opts) -> ConvertedMsg.
 
%% Commitment Management
-spec uncommitted(Msg) -> UncommittedMsg.
-spec uncommitted(Msg, Opts) -> UncommittedMsg.
-spec committed(Msg, CommittersSpec, Opts) -> [Key].
-spec commit(Msg, Wallet) -> SignedMsg.
-spec commit(Msg, Wallet, Opts) -> SignedMsg.
-spec normalize_commitments(Msg, Opts) -> NormalizedMsg.
 
%% Verification
-spec verify(Msg) -> boolean().
-spec verify(Msg, Committers) -> boolean().
-spec verify(Msg, Committers, Opts) -> boolean().
 
%% Commitment Queries
-spec signers(Msg, Opts) -> [Address].
-spec commitment(IDOrSpec, Msg) -> Result.
-spec commitment(IDOrSpec, Msg, Opts) -> Result.
-spec commitments(Spec, Msg, Opts) -> #{CommID => Commitment}.
-spec commitment_devices(Msg, Opts) -> [Device].
-spec is_signed_key(Key, Msg, Opts) -> boolean().
 
%% Message Filtering
-spec with_only_committers(Msg, Committers) -> FilteredMsg.
-spec with_only_committers(Msg, Committers, Opts) -> FilteredMsg.
-spec with_only_committed(Msg, Opts) -> {ok, OnlyCommittedMsg} | {error, Reason}.
-spec without_unless_signed(Keys, Msg, Opts) -> FilteredMsg.
-spec with_commitments(Spec, Msg, Opts) -> MsgWithCommitments.
-spec without_commitments(Spec, Msg, Opts) -> MsgWithoutCommitments.
 
%% Message Comparison
-spec match(Map1, Map2) -> true | {mismatch, Type, Path, Val1, Val2}.
-spec match(Map1, Map2, Mode) -> true | {mismatch, Type, Path, Val1, Val2}.
-spec match(Map1, Map2, Mode, Opts) -> true | {mismatch, Type, Path, Val1, Val2}.
-spec diff(Msg1, Msg2, Opts) -> DiffMap.
 
%% Utilities
-spec type(Msg) -> tx | binary | deep | shallow.
-spec minimize(Msg) -> MinimizedMsg.
-spec find_target(Self, Req, Opts) -> {ok, TargetMsg}.
-spec default_tx_list() -> [{Key, DefaultValue}].
-spec filter_default_keys(Map) -> FilteredMap.
-spec print(Msg) -> ok.

Public Functions

1. id/1, id/2, id/3

-spec id(Msg, Committers, Opts) -> ID
    when
        Msg :: map(),
        Committers :: none | uncommitted | all | signed | [Device],
        Opts :: map(),
        ID :: binary().

Description: Calculate message ID based on commitment spec. ID changes based on which parts are committed (signed).

Committer Options:
  • none / uncommitted / unsigned - ID of raw message (no signatures)
  • all / signed - ID including all signatures
  • [Device1, Device2] - ID with specific commitment devices
Test Code:
-module(hb_message_id_test).
-include_lib("eunit/include/eunit.hrl").
 
unsigned_id_test() ->
    Msg = #{<<"data">> => <<"value">>},
    UnsignedID = hb_message:id(Msg, unsigned, #{}),
    ?assert(is_binary(UnsignedID)),
    ?assertEqual(43, byte_size(UnsignedID)).  % Base64url encoded
 
signed_id_test() ->
    Wallet = ar_wallet:new(),
    Msg = #{<<"data">> => <<"value">>},
    Signed = hb_message:commit(Msg, #{priv_wallet => Wallet}),
    SignedID = hb_message:id(Signed, signed, #{}),
    UnsignedID = hb_message:id(Signed, unsigned, #{}),
    ?assertNotEqual(SignedID, UnsignedID).
 
deterministic_id_test() ->
    Msg = #{<<"key">> => <<"value">>},
    ID1 = hb_message:id(Msg),
    ID2 = hb_message:id(Msg),
    ?assertEqual(ID1, ID2).

2. convert/3, convert/4

-spec convert(Msg, TargetFormat, SourceFormat, Opts) -> ConvertedMsg
    when
        Msg :: term(),
        TargetFormat :: binary() | tabm,
        SourceFormat :: binary() | tabm,
        Opts :: map(),
        ConvertedMsg :: term().

Description: Convert message between formats via TABM intermediate representation.

Common Formats:
  • <<"structured@1.0">> - AO-Core structured messages
  • <<"ans104@1.0">> - ANS-104 data items
  • <<"httpsig@1.0">> - HTTP signed messages
  • <<"flat@1.0">> - Flat key-value maps
  • tabm - TABM format (internal)
Test Code:
-module(hb_message_convert_test).
-include_lib("eunit/include/eunit.hrl").
 
structured_to_tabm_test() ->
    Structured = #{<<"key">> => <<"value">>, <<"nested">> => #{<<"data">> => 123}},
    TABM = hb_message:convert(Structured, tabm, <<"structured@1.0">>, #{}),
    ?assert(is_map(TABM)),
    ?assertEqual(<<"value">>, maps:get(<<"key">>, TABM)).
 
tabm_to_structured_test() ->
    TABM = #{<<"key">> => <<"value">>},
    Structured = hb_message:convert(TABM, <<"structured@1.0">>, tabm, #{}),
    ?assert(is_map(Structured)),
    ?assertEqual(<<"value">>, maps:get(<<"key">>, Structured)).
 
ans104_roundtrip_test() ->
    Original = #{<<"data">> => <<"test">>},
    ANS104 = hb_message:convert(Original, <<"ans104@1.0">>, #{}),
    BackToStructured = hb_message:convert(ANS104, <<"structured@1.0">>, <<"ans104@1.0">>, #{}),
    ?assertEqual(<<"test">>, maps:get(<<"data">>, BackToStructured)).

3. commit/2, commit/3

-spec commit(Msg, Wallet, Opts) -> SignedMsg
    when
        Msg :: map(),
        Wallet :: {PrivKey, PubKey} | #{priv_wallet => {PrivKey, PubKey}},
        Opts :: map(),
        SignedMsg :: map().

Description: Sign a message using provided wallet. Adds cryptographic commitments to message.

Test Code:
-module(hb_message_commit_test).
-include_lib("eunit/include/eunit.hrl").
 
commit_message_test() ->
    Wallet = ar_wallet:new(),
    Msg = #{<<"data">> => <<"value">>},
    Signed = hb_message:commit(Msg, #{priv_wallet => Wallet}),
    ?assert(maps:is_key(<<"commitments">>, Signed)),
    ?assert(hb_message:verify(Signed, all, #{})).
 
commit_with_device_test() ->
    Wallet = ar_wallet:new(),
    Msg = #{<<"data">> => <<"value">>},
    Opts = #{<<"device">> => <<"ans104@1.0">>},
    Signed = hb_message:commit(Msg, #{priv_wallet => Wallet}, Opts),
    ?assert(hb_message:verify(Signed)).

4. verify/1, verify/2, verify/3

-spec verify(Msg, Committers, Opts) -> boolean()
    when
        Msg :: map(),
        Committers :: all | [Device],
        Opts :: map().

Description: Verify cryptographic commitments in message. Returns true if all specified commitments are valid. Messages without commitments return true (nothing to fail).

Test Code:
-module(hb_message_verify_test).
-include_lib("eunit/include/eunit.hrl").
 
verify_signed_test() ->
    Wallet = ar_wallet:new(),
    Msg = hb_message:commit(#{<<"data">> => <<"value">>}, #{priv_wallet => Wallet}),
    ?assert(hb_message:verify(Msg, all, #{})).
 
verify_unsigned_test() ->
    Msg = #{<<"data">> => <<"value">>},
    %% Unsigned message has no commitments, so verify returns true (nothing to fail)
    ?assert(hb_message:verify(Msg, all, #{})),
    %% But it has no signers
    ?assertEqual([], hb_message:signers(Msg, #{})).
 
verify_tampered_test() ->
    Wallet = ar_wallet:new(),
    Signed = hb_message:commit(#{<<"data">> => <<"value">>}, #{priv_wallet => Wallet}),
    Tampered = Signed#{<<"data">> => <<"modified">>},
    ?assertNot(hb_message:verify(Tampered, all, #{})).

5. uncommitted/1, uncommitted/2

-spec uncommitted(Msg, Opts) -> UncommittedMsg
    when
        Msg :: map(),
        Opts :: map(),
        UncommittedMsg :: map().

Description: Extract uncommitted (unsigned) portion of message, removing commitments.

Test Code:
-module(hb_message_uncommitted_test).
-include_lib("eunit/include/eunit.hrl").
 
uncommitted_removes_sigs_test() ->
    Wallet = ar_wallet:new(),
    Original = #{<<"data">> => <<"value">>},
    Signed = hb_message:commit(Original, #{priv_wallet => Wallet}),
    Uncommitted = hb_message:uncommitted(Signed),
    ?assertNot(maps:is_key(<<"commitments">>, Uncommitted)),
    ?assertEqual(<<"value">>, maps:get(<<"data">>, Uncommitted)).

6. committed/3

-spec committed(Msg, CommittersSpec, Opts) -> [binary()]
    when
        Msg :: map(),
        CommittersSpec :: all | none | [CommitterID] | map(),
        Opts :: map().

Description: Return the list of keys that are covered by commitments.

Test Code:
committed_keys_test() ->
    Wallet = ar_wallet:new(),
    Signed = hb_message:commit(#{<<"data">> => <<"test">>}, #{priv_wallet => Wallet}),
    Keys = hb_message:committed(Signed, all, #{}),
    ?assert(is_list(Keys)),
    ?assert(lists:member(<<"data">>, Keys)).

7. signers/2

-spec signers(Msg, Opts) -> [binary()]
    when
        Msg :: map(),
        Opts :: map().

Description: Return addresses of all committers with standard 256-bit addresses.

Test Code:
signers_test() ->
    Wallet = ar_wallet:new(),
    Signed = hb_message:commit(#{<<"data">> => <<"test">>}, #{priv_wallet => Wallet}),
    Signers = hb_message:signers(Signed, #{}),
    ?assert(is_list(Signers)),
    ?assert(length(Signers) >= 1).

8. type/1

-spec type(Msg) -> tx | binary | deep | shallow
    when
        Msg :: term().

Description: Determine the type of an encoded message.

Test Code:
type_test() ->
    ?assertEqual(shallow, hb_message:type(#{<<"key">> => <<"value">>})),
    ?assertEqual(deep, hb_message:type(#{<<"nested">> => #{<<"key">> => <<"value">>}})),
    ?assertEqual(binary, hb_message:type(<<"raw">>)).

9. minimize/1

-spec minimize(Msg) -> map()
    when
        Msg :: map().

Description: Remove keys that can be regenerated (unsigned_id, content-digest) and private keys.

Test Code:
minimize_test() ->
    Msg = #{<<"data">> => <<"test">>, <<"unsigned_id">> => <<"abc">>, <<"priv">> => #{}},
    Min = hb_message:minimize(Msg),
    ?assert(maps:is_key(<<"data">>, Min)),
    ?assertNot(maps:is_key(<<"unsigned_id">>, Min)),
    ?assertNot(maps:is_key(<<"priv">>, Min)).

10. normalize_commitments/2

-spec normalize_commitments(Msg, Opts) -> map()
    when
        Msg :: map(),
        Opts :: map().

Description: Ensure message has at least one unsigned ID present in commitments. Forces ID calculation work to happen strategically.

Test Code:
normalize_commitments_test() ->
    Msg = #{<<"data">> => <<"test">>},
    Normalized = hb_message:normalize_commitments(Msg, #{}),
    ?assert(maps:is_key(<<"commitments">>, Normalized)).

11. is_signed_key/3

-spec is_signed_key(Key, Msg, Opts) -> boolean()
    when
        Key :: binary(),
        Msg :: map(),
        Opts :: map().

Description: Check if a specific key is covered by message commitments.

Test Code:
is_signed_key_test() ->
    Wallet = ar_wallet:new(),
    Signed = hb_message:commit(#{<<"data">> => <<"test">>}, #{priv_wallet => Wallet}),
    ?assert(hb_message:is_signed_key(<<"data">>, Signed, #{})).

12. commitment/2, commitment/3

-spec commitment(IDOrSpec, Msg, Opts) -> Commitment | not_found | {ok, CommID, Commitment} | multiple_matches
    when
        IDOrSpec :: binary() | map(),
        Msg :: map(),
        Opts :: map(),
        Commitment :: map().

Description: Extract a single commitment by ID or spec. When given an existing commitment ID, returns the commitment map directly. When given a spec, returns not_found, {ok, CommID, Commitment}, or multiple_matches. Use commitments/3 when expecting multiple matches.

Test Code:
commitment_by_id_test() ->
    Wallet = ar_wallet:new(),
    Signed = hb_message:commit(#{<<"data">> => <<"test">>}, #{priv_wallet => Wallet}),
    Comms = maps:get(<<"commitments">>, Signed),
    [FirstID | _] = maps:keys(Comms),
    %% When given an existing ID, returns the commitment map directly
    Result = hb_message:commitment(FirstID, Signed, #{}),
    ?assert(is_map(Result)),
    ?assert(maps:is_key(<<"signature">>, Result)).
 
commitment_by_spec_test() ->
    Wallet = ar_wallet:new(),
    Signed = hb_message:commit(#{<<"data">> => <<"test">>}, #{priv_wallet => Wallet}),
    %% Use commitments/3 to get all matching commitments by device
    Matches = hb_message:commitments(#{<<"commitment-device">> => <<"httpsig@1.0">>}, Signed, #{}),
    ?assert(map_size(Matches) >= 1).

13. commitments/3

-spec commitments(Spec, Msg, Opts) -> #{CommID => Commitment}
    when
        Spec :: binary() | map(),
        Msg :: map(),
        Opts :: map().

Description: Return all commitments matching a spec.

Test Code:
commitments_test() ->
    Wallet = ar_wallet:new(),
    Signed = hb_message:commit(#{<<"data">> => <<"test">>}, #{priv_wallet => Wallet}),
    %% Filter commitments by type
    Matches = hb_message:commitments(#{<<"type">> => <<"rsa-pss-sha512">>}, Signed, #{}),
    ?assert(is_map(Matches)).

14. commitment_devices/2

-spec commitment_devices(Msg, Opts) -> [binary()]
    when
        Msg :: map(),
        Opts :: map().

Description: Return the devices used to create commitments on a message.

Test Code:
commitment_devices_test() ->
    Wallet = ar_wallet:new(),
    Signed = hb_message:commit(#{<<"data">> => <<"test">>}, #{priv_wallet => Wallet}),
    Devices = hb_message:commitment_devices(Signed, #{}),
    ?assert(is_list(Devices)).

15. with_only_committers/2, with_only_committers/3

-spec with_only_committers(Msg, Committers, Opts) -> map()
    when
        Msg :: map(),
        Committers :: [binary()],
        Opts :: map().

Description: Filter message to include only specified committers' commitments.

Test Code:
with_only_committers_test() ->
    Wallet = ar_wallet:new(),
    Signed = hb_message:commit(#{<<"data">> => <<"test">>}, #{priv_wallet => Wallet}),
    Address = ar_wallet:to_address(Wallet),
    Filtered = hb_message:with_only_committers(Signed, [Address]),
    ?assert(maps:is_key(<<"commitments">>, Filtered)).

16. with_only_committed/2

-spec with_only_committed(Msg, Opts) -> {ok, map()} | {error, Reason}
    when
        Msg :: map(),
        Opts :: map().

Description: Return message with only keys that are covered by commitments. Must verify message separately.

Test Code:
with_only_committed_test() ->
    Wallet = ar_wallet:new(),
    Signed = hb_message:commit(#{<<"data">> => <<"test">>}, #{priv_wallet => Wallet}),
    {ok, Committed} = hb_message:with_only_committed(Signed, #{}),
    ?assert(maps:is_key(<<"data">>, Committed)).

17. without_unless_signed/3

-spec without_unless_signed(Keys, Msg, Opts) -> map()
    when
        Keys :: binary() | [binary()],
        Msg :: map(),
        Opts :: map().

Description: Remove specified keys unless they are signed.

Test Code:
without_unless_signed_test() ->
    Wallet = ar_wallet:new(),
    Signed = hb_message:commit(#{<<"data">> => <<"test">>}, #{priv_wallet => Wallet}),
    WithExtra = Signed#{<<"unsigned_key">> => <<"extra">>},
    Filtered = hb_message:without_unless_signed([<<"unsigned_key">>], WithExtra, #{}),
    ?assertNot(maps:is_key(<<"unsigned_key">>, Filtered)).

18. with_commitments/3

-spec with_commitments(Spec, Msg, Opts) -> map()
    when
        Spec :: binary() | [binary()] | map(),
        Msg :: map(),
        Opts :: map().

Description: Filter message commitments to only those matching a spec.

Test Code:
with_commitments_test() ->
    Wallet = ar_wallet:new(),
    Signed = hb_message:commit(#{<<"data">> => <<"test">>}, #{priv_wallet => Wallet}),
    %% Get first commitment ID
    Comms = maps:get(<<"commitments">>, Signed, #{}),
    [FirstID | _] = maps:keys(Comms),
    Filtered = hb_message:with_commitments([FirstID], Signed, #{}),
    ?assert(maps:is_key(<<"commitments">>, Filtered)).

19. without_commitments/3

-spec without_commitments(Spec, Msg, Opts) -> map()
    when
        Spec :: binary() | [binary()] | map(),
        Msg :: map(),
        Opts :: map().

Description: Remove commitments matching a spec from message.

Test Code:
without_commitments_test() ->
    Wallet = ar_wallet:new(),
    Signed = hb_message:commit(#{<<"data">> => <<"test">>}, #{priv_wallet => Wallet}),
    %% Get all commitment IDs and remove them
    Comms = maps:get(<<"commitments">>, Signed, #{}),
    CommIDs = maps:keys(Comms),
    Filtered = hb_message:without_commitments(CommIDs, Signed, #{}),
    RemainingComms = maps:get(<<"commitments">>, Filtered, #{}),
    ?assertEqual(0, map_size(RemainingComms)).

20. match/2, match/3, match/4

-spec match(Map1, Map2, Mode, Opts) -> true | {mismatch, Type, Path, Val1, Val2}
    when
        Map1 :: map(),
        Map2 :: map(),
        Mode :: strict | only_present | primary,
        Opts :: map().

Description: Check if two messages match, with configurable matching modes.

Modes:
  • strict - All keys in both maps must be present and match
  • only_present - Only present keys in both maps must match
  • primary - Only the primary map's keys must be present
Test Code:
match_strict_test() ->
    Msg1 = #{<<"a">> => <<"1">>},
    Msg2 = #{<<"a">> => <<"1">>},
    ?assertEqual(true, hb_message:match(Msg1, Msg2, strict, #{})).
 
match_mismatch_test() ->
    Msg1 = #{<<"a">> => <<"1">>},
    Msg2 = #{<<"a">> => <<"2">>},
    ?assertMatch({mismatch, _, _, _, _}, hb_message:match(Msg1, Msg2)).
 
match_only_present_test() ->
    Msg1 = #{<<"a">> => <<"1">>},
    Msg2 = #{<<"a">> => <<"1">>, <<"b">> => <<"2">>},
    ?assertEqual(true, hb_message:match(Msg1, Msg2, only_present, #{})).

21. diff/3

-spec diff(Msg1, Msg2, Opts) -> map() | not_found
    when
        Msg1 :: map(),
        Msg2 :: map(),
        Opts :: map().

Description: Return numeric differences between two messages, recursively comparing nested maps. Non-numeric changes return the new value. Keys only in first message are dropped.

Test Code:
diff_numeric_test() ->
    Msg1 = #{<<"count">> => 10},
    Msg2 = #{<<"count">> => 15},
    Diff = hb_message:diff(Msg1, Msg2, #{}),
    ?assertEqual(5, maps:get(<<"count">>, Diff)).
 
diff_new_key_test() ->
    Msg1 = #{<<"a">> => <<"1">>},
    Msg2 = #{<<"a">> => <<"1">>, <<"b">> => <<"new">>},
    Diff = hb_message:diff(Msg1, Msg2, #{}),
    ?assertEqual(<<"new">>, maps:get(<<"b">>, Diff)).

22. find_target/3

-spec find_target(Self, Req, Opts) -> {ok, TargetMsg}
    when
        Self :: map(),
        Req :: map(),
        Opts :: map().

Description: Find the target message for an operation. Looks for target key in request; returns self if not present or if target is <<"self">>.

Test Code:
find_target_self_test() ->
    Self = #{<<"data">> => <<"self">>},
    Req = #{},
    {ok, Target} = hb_message:find_target(Self, Req, #{}),
    ?assertEqual(Self, Target).
 
find_target_key_test() ->
    Self = #{<<"data">> => <<"self">>},
    Req = #{<<"target">> => <<"other">>, <<"other">> => #{<<"data">> => <<"other">>}},
    {ok, Target} = hb_message:find_target(Self, Req, #{}),
    ?assertEqual(#{<<"data">> => <<"other">>}, Target).

23. default_tx_list/0

-spec default_tx_list() -> [{binary(), term()}].

Description: Return ordered list of #tx{} record fields with their default values as AO-Core keys.

Test Code:
default_tx_list_test() ->
    List = hb_message:default_tx_list(),
    ?assert(is_list(List)),
    ?assert(length(List) > 0),
    [{FirstKey, _} | _] = List,
    ?assert(is_binary(FirstKey)).

24. filter_default_keys/1

-spec filter_default_keys(Map) -> map()
    when
        Map :: map().

Description: Remove keys from a map that have default #tx{} record values.

Test Code:
filter_default_keys_test() ->
    %% quantity = 0 is a default
    Msg = #{<<"quantity">> => 0, <<"data">> => <<"test">>},
    Filtered = hb_message:filter_default_keys(Msg),
    ?assertNot(maps:is_key(<<"quantity">>, Filtered)),
    ?assert(maps:is_key(<<"data">>, Filtered)).

25. print/1

-spec print(Msg) -> ok
    when
        Msg :: map().

Description: Pretty-print a message to standard error for debugging.

Test Code:
print_test() ->
    Msg = #{<<"data">> => <<"test">>},
    ?assertEqual(ok, hb_message:print(Msg)).

Commitment System

Commitment Structure

#{
    <<"data">> => <<"value">>,
    <<"commitments">> => #{
        <<"Device1">> => #{
            <<"signature">> => Signature,
            <<"public-key">> => PublicKey,
            <<"commitment">> => SignedData
        }
    }
}

Commitment Devices

Common Devices:
  • <<"httpsig@1.0">> - HTTP signatures
  • <<"ans104@1.0">> - ANS-104 signatures
  • Custom devices can add commitments

Common Patterns

%% Convert to TABM for processing
TABM = hb_message:convert(Msg, tabm, #{}),
 
%% Convert between formats
ANS104 = hb_message:convert(Structured, <<"ans104@1.0">>, #{}),
 
%% Sign message
Wallet = ar_wallet:new(),
Signed = hb_message:commit(Msg, #{priv_wallet => Wallet}),
 
%% Verify signature
IsValid = hb_message:verify(Signed, all, #{}),
 
%% Get message ID
UnsignedID = hb_message:id(Msg, unsigned, #{}),
SignedID = hb_message:id(Msg, signed, #{}),
 
%% Extract unsigned portion
Original = hb_message:uncommitted(SignedMsg),
 
%% Get signers
Signers = hb_message:signers(SignedMsg, #{}),
 
%% Filter to committed keys only
Committed = hb_message:with_only_committed(Msg, #{}),
 
%% Check message type
Type = hb_message:type(Msg),  % structured | ans104 | httpsig | flat
 
%% Minimize message (remove defaults)
Minimal = hb_message:minimize(Msg),
 
%% Print for debugging
hb_message:print(Msg).

Message Type Detection

type(Msg) ->
    case {is_record(Msg, tx), is_map(Msg)} of
        {true, _} -> ans104;
        {_, true} ->
            case maps:is_key(<<"commitments">>, Msg) of
                true -> httpsig;
                false ->
                    case has_type_annotations(Msg) of
                        true -> structured;
                        false -> flat
                    end
            end;
        _ -> unknown
    end.

References

  • Codecs - dev_codec_*.erl modules
  • AO Core - hb_ao.erl
  • Cache - hb_cache.erl
  • Test Vectors - hb_message_test_vectors.erl
  • Arweave - ar_wallet.erl, ar_bundles.erl

Notes

  1. TABM Central: All conversions go through TABM format
  2. Codec System: Each format has codec module (dev_codec_*)
  3. ID Calculation: Depends on commitment specification
  4. Signature Support: Multiple commitment devices per message
  5. Priv Preservation: Private data preserved across conversions
  6. Two-Phase Convert: Input→TABM, TABM→Output
  7. Verification: Check all or specific commitment devices
  8. Uncommitted: Extract original data without signatures
  9. Type Detection: Automatic format detection
  10. Minimize: Remove default/unnecessary fields
  11. Test Vectors: Comprehensive tests in separate module
  12. Use hb_ao: Prefer hb_ao for normal message operations
  13. Low-level API: Direct use rare, mainly for codec authors
  14. Format Agnostic: Works with any registered codec
  15. Commitment Devices: Extensible signature system