Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
654af74
feat: support underscore name subdomains
charmful0x Jul 27, 2026
a093cc4
wip: `index` route validation
samcamwilliams Jul 27, 2026
de4dfc3
refactor(copycat): share the local item-caching helper
samcamwilliams Jul 16, 2026
b2c741f
refactor(scheduler): extract the logic shared by scheduler devices in…
samcamwilliams Jul 16, 2026
458aff8
refactor(arweave): share best_response via lib_arweave_common
samcamwilliams Jul 16, 2026
c2d67c5
fix(query): resolve `block` on ~query@1.0 transaction nodes
samcamwilliams Jul 16, 2026
9f77c70
feat: add ~arweave-scheduler@1.0, an Arweave-L1-sequenced scheduler
samcamwilliams Jul 16, 2026
df13f2d
feat(arweave-scheduler): sequence a process by every L1 transaction
samcamwilliams Jul 25, 2026
bd4c819
fix(arweave-scheduler): read a header without reaching for its data
samcamwilliams Jul 25, 2026
3725646
feat: add ~arweave-swap@1.0, selling process tokens for native AR
samcamwilliams Jul 25, 2026
37d514e
fix: read payment from L1 tx fields
charmful0x Jul 25, 2026
4798e3b
fix: 43 chars test ?PROCESS
charmful0x Jul 25, 2026
b3094cc
chore: defensive ignoring of duplicate OrderID make_offer/4
charmful0x Jul 25, 2026
b320bd7
feat(arweave-swap): let an order charge to register interest
samcamwilliams Jul 25, 2026
e6bb63d
feat: add ~name-token@1.0, a name whose holder says what it means
samcamwilliams Jul 25, 2026
fb74556
feat(name-token): seed a name's holder and value at spawn
samcamwilliams Jul 26, 2026
f15d4da
fix: read a stranger's message as a message, not as their device
samcamwilliams Jul 26, 2026
07bad76
fix: read scheduled transaction fields as data
samcamwilliams Jul 26, 2026
8d55279
fix: read foreign transaction metadata passively
samcamwilliams Jul 26, 2026
cdc4c74
feat(arweave-swap): the buyer buys the exclusive right, and stakes fo…
samcamwilliams Jul 27, 2026
fe7bc04
feat(copycat): add headers-only indexing mode
samcamwilliams Jul 27, 2026
3fdab35
refactor(copycat): simplify headers-only indexing
samcamwilliams Jul 27, 2026
d4ee9ad
refactor(name-token): minimize sale stack
samcamwilliams Jul 27, 2026
6214074
feat: isolate process store
jfrain99 Jul 27, 2026
412d77a
fix: add back process store scope
jfrain99 Jul 27, 2026
6674fc2
fix: fix process cache test suite to run on module call
jfrain99 Jul 27, 2026
36418d4
feat: max age for cached process now requests
jfrain99 Jul 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/core/resolver/hb_opts.erl
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,8 @@ raw_default_message() ->
% default_index => #{ <<"device">> => <<"hyperbuddy@1.0">> },
% Should we use the latest cached state of a process when computing?
<<"process-now-from-cache">> => false,
% Maximum age, in seconds, for `/now' to serve from the process cache.
<<"process-now-max-age">> => infinity,
% Should we trust the GraphQL API when converting to ANS-104? Some GQL
% services do not provide the `anchor' or `last_tx' fields, so their
% responses are not verifiable.
Expand Down
28 changes: 2 additions & 26 deletions src/preloaded/arweave/dev_arweave.erl
Original file line number Diff line number Diff line change
Expand Up @@ -885,31 +885,7 @@ request(Method, Path, Extra, LogExtra, Opts) ->
<<"cache-control">> => [<<"no-cache">>, <<"no-store">>]
}
),
to_message(Path, Method, best_response(Res), LogExtra, Opts).

%% @doc Select the best response from a list of responses by sorting them
%% ascending by HTTP status code. Returns the first (best) response tuple.
best_response({error, {no_viable_responses, Responses}}) ->
best_response(Responses);
best_response([]) ->
{error, no_viable_responses};
best_response(Responses) when is_list(Responses) ->
Sorted = lists:sort(
fun({_, ResponseA}, {_, ResponseB}) ->
StatusA = response_status(ResponseA),
StatusB = response_status(ResponseB),
StatusA =< StatusB
end,
Responses
),
hd(Sorted);
best_response(Response) ->
Response.

response_status(Response) when is_map(Response) ->
maps:get(<<"status">>, Response, 999);
response_status(_Response) ->
999.
to_message(Path, Method, lib_arweave_common:best_response(Res), LogExtra, Opts).

%% @doc Transform a response from the Arweave node into an AO-Core message.
to_message(Path, Method, {error, #{ <<"status">> := 404 }}, LogExtra, _Opts) ->
Expand Down Expand Up @@ -1206,7 +1182,7 @@ best_response_handles_failed_connect_entries_test_parallel() ->
],
?assertEqual(
{ok, #{ <<"status">> => 200, <<"body">> => <<"OK-2">> }},
best_response(Responses)
lib_arweave_common:best_response(Responses)
).

best_response_non_map_error_round_trips_test_parallel() ->
Expand Down
35 changes: 31 additions & 4 deletions src/preloaded/arweave/dev_manifest.erl
Original file line number Diff line number Diff line change
Expand Up @@ -102,10 +102,7 @@ request(Base, Req, Opts) ->
% message replaced with the casted manifest.
case {Rest, maybe_cast_manifest(Loaded, Opts)} of
{_, ignored} ->
?event(
debug_manifest,
{non_manifest_returning_loaded, {loaded, Loaded}, {rest, Rest}}),
{ok, Req#{ <<"body">> => [Loaded|Rest] }};
maybe_cast_resolved_path(Loaded, Rest, Req, Opts);
{[], {ok, Casted}} ->
?event(debug_manifest, {manifest_returning_index, {req, Req}}),
{ok, Req#{ <<"body">> => [Casted, #{<<"path">> => <<"index">>}] }};
Expand All @@ -129,6 +126,36 @@ request(Base, Req, Opts) ->
{ok, Req}
end.

maybe_cast_resolved_path(Loaded, [Next | Rest], Req, Opts) ->
case catch hb_ao:resolve(Loaded, Next, Opts) of
{ok, Resolved} ->
case catch load(Resolved, Opts) of
{ok, NextLoaded} ->
case maybe_cast_manifest(NextLoaded, Opts) of
{ok, Casted} -> manifest_path(Casted, Rest, Req);
ignored -> non_manifest_path(Loaded, [Next | Rest], Req)
end;
_ -> non_manifest_path(Loaded, [Next | Rest], Req)
end;
_ -> non_manifest_path(Loaded, [Next | Rest], Req)
end;
maybe_cast_resolved_path(Loaded, [], Req, _Opts) ->
non_manifest_path(Loaded, [], Req).

manifest_path(Casted, [], Req) ->
?event(debug_manifest, {manifest_returning_index, {req, Req}}),
{ok, Req#{ <<"body">> => [Casted, #{<<"path">> => <<"index">>}] }};
manifest_path(Casted, Rest, Req) ->
?event(debug_manifest, {manifest_returning_subpath, {req, Req}}),
{ok, Req#{ <<"body">> => [Casted | Rest] }}.

non_manifest_path(Loaded, Rest, Req) ->
?event(
debug_manifest,
{non_manifest_returning_loaded, {loaded, Loaded}, {rest, Rest}}
),
{ok, Req#{ <<"body">> => [Loaded | Rest] }}.

%% @doc Cast a message to `manifest@1.0` if it has the correct content-type but
%% no other device is specified.
load(Msg, _Opts) when is_map(Msg) -> {ok, Msg};
Expand Down
27 changes: 27 additions & 0 deletions src/preloaded/codec/lib_arweave_common.erl
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
-export([bundle_hint/4, data/3, tags/5, excluded_tags/3]).
-export([to/3, to/6, siginfo/4, fields_to_tx/4]).
-export([bundle_header/2, bundle_header/3]).
-export([best_response/1]).
-include("include/hb.hrl").

-define(ANS104_BASE_FIELDS, [<<"anchor">>, <<"target">>]).
Expand Down Expand Up @@ -770,3 +771,29 @@ read_bundle_header(BundleStartOffset, HeaderSize, FirstChunk, Opts) ->
Error ->
Error
end.

%% @doc Select the best response from a (potentially multi-node) request result
%% by sorting the responses ascending by HTTP status code and returning the
%% first (best) one. Shared by the Arweave devices, which broadcast requests to
%% several nodes and take the most successful reply.
best_response({error, {no_viable_responses, Responses}}) ->
best_response(Responses);
best_response([]) ->
{error, no_viable_responses};
best_response(Responses) when is_list(Responses) ->
Sorted = lists:sort(
fun({_, ResponseA}, {_, ResponseB}) ->
StatusA = response_status(ResponseA),
StatusB = response_status(ResponseB),
StatusA =< StatusB
end,
Responses
),
hd(Sorted);
best_response(Response) ->
Response.

response_status(Response) when is_map(Response) ->
maps:get(<<"status">>, Response, 999);
response_status(_Response) ->
999.
99 changes: 88 additions & 11 deletions src/preloaded/name/dev_name.erl
Original file line number Diff line number Diff line change
Expand Up @@ -83,21 +83,24 @@ request(HookMsg, HookReq, Opts) ->
maybe
{ok, Req} ?= hb_maps:find(<<"request">>, HookReq, Opts),
{ok, Host} ?= hb_maps:find(<<"host">>, Req, Opts),
{ok, Name} ?=
{ok, RawName} ?=
name_from_host(
Host,
hb_opts:get(node_host, hb_opts:get(host, no_host, Opts), Opts)
),
{Name, NamePath} = name_path(RawName),
{ok, ResolvedMsg} ?= resolve(Name, HookMsg, HookReq, Opts),
ModReq =
maybe_append_named_message(
append_name_path(
ResolvedMsg,
NamePath,
hb_util:ok(hb_maps:find(<<"body">>, HookReq, Opts)),
Opts
),
?event(
{request_with_prepended_path,
{name, Name},
{name_path, NamePath},
{full_host, Host},
{resolved_msg, ResolvedMsg},
{to_execute, ModReq}
Expand All @@ -121,6 +124,19 @@ request(HookMsg, HookReq, Opts) ->
end
end.

%% @doc Split an underscore-encoded subdomain into root name and path parts.
%% `x_y_z' resolves `z', then `y', then `x'.
name_path(Name) ->
case lists:reverse(binary:split(Name, <<"_">>, [global, trim_all])) of
[Root | Path] -> {Root, Path};
[] -> {Name, []}
end.

%% @doc Append the resolved name as the base, then any path encoded in the host.
append_name_path(ResolvedMsg, NamePath, OldReq, Opts) ->
[Base | Rest] = maybe_append_named_message(ResolvedMsg, OldReq, Opts),
[Base | ([#{ <<"path">> => Part } || Part <- NamePath] ++ Rest)].

%% @doc After finding a hit for a named message, we should ensure that it is the
%% base message for the evaluation. If it is already present in the request,
%% however, we should not add it twice. Instead, we must add the version that
Expand Down Expand Up @@ -209,6 +225,19 @@ device_resolver(Msg) ->
}
}.

host_test_opts() ->
#{
<<"port">> => 0,
<<"name-resolvers">> =>
[
device_resolver(
#{ <<"permabytes">> => #{ <<"content-type">> => <<"text/html">> } }
)
],
<<"on">> =>
#{ <<"request">> => #{ <<"device">> => <<"name@1.0">> } }
}.

single_resolver_test_parallel() ->
?assertEqual(
{ok, <<"world">>},
Expand Down Expand Up @@ -303,39 +332,82 @@ arns_json_snapshot_test_parallel() ->
)
).

arns_host_resolution_test_parallel() ->
Opts = hb_name_test_utils:arns_opts(),
host_resolution_test_parallel() ->
Opts = host_test_opts(),
Node = hb_http_server:start_node(Opts),
?assertMatch(
{ok, <<"text/html">>},
hb_http:get(
Node,
#{
<<"path">> => <<"content-type">>,
<<"host">> => <<"001_permabytes.localhost">>
<<"host">> => <<"permabytes.localhost">>
},
Opts
)
).

arns_host_resolution_with_node_host_test_parallel() ->
Opts = (hb_name_test_utils:arns_opts())#{
<<"node-host">> => <<"http://localhost">>,
<<"port">> => 0
},
host_resolution_with_node_host_test_parallel() ->
Opts = (host_test_opts())#{ <<"node-host">> => <<"http://localhost">> },
Node = hb_http_server:start_node(Opts),
?assertMatch(
{ok, <<"text/html">>},
hb_http:get(
Node,
#{
<<"path">> => <<"content-type">>,
<<"host">> => <<"001_permabytes.localhost">>
<<"host">> => <<"permabytes.localhost">>
},
Opts
)
).

underscore_host_parts_resolve_manifest_test_parallel() ->
Opts = hb_name_test_utils:manifest_opts(),
ManifestID = <<"42jky7O3rzKkMOfHBXgK-304YjulzEYqHc9qyjT3efA">>,
{ok, IndexPageID} =
hb_cache:write(
#{
<<"content-type">> => <<"text/html">>,
<<"body">> => <<"Index page.">>
}
),
Subrealm =
#{
<<"sub2">> => ManifestID
<<"index">> => IndexPageID
}
Opts =
(hb_name_test_utils:manifest_opts())#{
<<"port">> => 0,
<<"http-client-hackney-recv-timeout">> => 30_000,
<<"name-resolvers">> =>
[
device_resolver(
#{
<<"sub2_sub1">> => <<"not-the-manifest">>,
<<"sub1">> => IndexPageID
}
)
],
<<"on">> =>
#{
<<"request">> =>
[
#{<<"device">> => <<"name@1.0">>},
#{<<"device">> => <<"manifest@1.0">>}
]
}
},
Node = hb_http_server:start_node(Opts),
hb_test_utils:assert_manifest_response(
Node,
#{ <<"path">> => <<"/">>, <<"host">> => <<"sub2_sub1.localhost">> },
<<"text/html">>,
<<"<title>Portal</title>">>,
Opts
).

root_request_skips_name_resolution_test_parallel() ->
BaseOpts =
#{
Expand Down Expand Up @@ -371,4 +443,9 @@ name_from_host_test_parallel() ->
?assertEqual(
{ok, <<"sub3.sub2">>},
name_from_host(<<"sub3.sub2.sub1.abc.xyz">>, <<"sub1.abc.xyz">>)
),
?assertEqual({<<"sub1">>, [<<"sub2">>]}, name_path(<<"sub2_sub1">>)),
?assertEqual(
{<<"sub1">>, [<<"sub2">>, <<"sub3">>]},
name_path(<<"sub3_sub2_sub1">>)
).
Loading