commit ec4fcf15cf05bf29e572760af7cf132cb0050ede
parent 2398cc0605ad6fbb5729b9f1867cc53e3e8bbede
Author: triesap <tyson@radroots.org>
Date: Mon, 3 Aug 2026 06:57:51 +0000
transport-nostr: remove storage and outbox dependencies
- delete superseded fetch, outbox, publish, relay-document, and outcome implementations
- constrain the adapter source inventory to signer-free transport responsibilities
- realign architecture witnesses with the final split source and sink interfaces
- refresh governed source-maintenance artifacts after validator authority changes
Diffstat:
14 files changed, 97 insertions(+), 4782 deletions(-)
diff --git a/crates/event_store/contracts/source_maintenance_v1.manifest.json b/crates/event_store/contracts/source_maintenance_v1.manifest.json
@@ -873,8 +873,8 @@
{
"role": "contract_command_authority",
"path": "tools/xtask/src/contract.rs",
- "byte_length": 508073,
- "sha256": "26d1825a0d7a4a3bb983478f2ef87dc228cbfb68f794bb52864e34a62ef1eead",
+ "byte_length": 507854,
+ "sha256": "92b1fd2cea9f3015285000550a75ca123ffe21f46556e8a4d81b4e3c180f0586",
"hash_algorithm": "sha256_bytes_v1"
},
{
diff --git a/crates/event_store/contracts/source_maintenance_v1.manifest.sha256 b/crates/event_store/contracts/source_maintenance_v1.manifest.sha256
@@ -1 +1 @@
-194c8175537655f006e1bc778b6588703001cb4fec382cbdbc5fb1d011ee6500
+ba95f802e05eb85c5b2ffb65da747e503a7f8d58849096bc7835a30cee0c392f
diff --git a/crates/event_store/src/generated/source_maintenance_manifest.rs b/crates/event_store/src/generated/source_maintenance_manifest.rs
@@ -1,8 +1,8 @@
// @generated by `cargo xtask contract source-maintenance-manifest --write`; do not edit.
-pub(crate) const SOURCE_MAINTENANCE_MANIFEST_JSON: &str = "{\n \"schema_version\": 1,\n \"contract_id\": \"radroots_event_store.source_maintenance_v1\",\n \"hook_id\": \"source_maintenance_v1\",\n \"manifest_schema\": {\n \"path\": \"crates/event_store/contracts/source_maintenance_v1.manifest.schema.json\",\n \"byte_length\": 12315,\n \"sha256\": \"96ea98a5abf3348b61565954cd34f2c622411d8dd34205ef04134bbe0e3d3f2b\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n \"predecessor\": {\n \"hook_id\": \"food_availability_projection_v1\",\n \"manifest\": {\n \"path\": \"crates/event_store/contracts/food_availability_projection_v1.manifest.json\",\n \"byte_length\": 17455,\n \"sha256\": \"02dfe1b450fbdac16e718888215b4dd5c85d8975440fa21e8f439fb24c2b2990\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n }\n },\n \"migration\": {\n \"version\": 4,\n \"name\": \"source_maintenance\",\n \"up\": {\n \"path\": \"crates/event_store/migrations/0004_source_maintenance.up.sql\",\n \"byte_length\": 19841,\n \"sha256\": \"ab2724188f8d08c897eebea2533a635e7c74282a25e84e4c0c37e78b08837a43\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n \"down\": {\n \"path\": \"crates/event_store/migrations/0004_source_maintenance.down.sql\",\n \"byte_length\": 5172,\n \"sha256\": \"fe44fd53c51545c08ea479b385e6781079dab70fc63da2a3c205d727a00ce860\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n \"schema_sha256\": \"074f85b663444ac150239ecd8441ea4a96ad83a798a55e22d2e5e2f7ee943a8c\",\n \"catalog\": {\n \"objects\": [\n \"radroots_event_store_source_capacity_delete_guard\",\n \"radroots_event_store_source_capacity_insert_guard\",\n \"radroots_event_store_source_capacity_marker_close_guard\",\n \"radroots_event_store_source_capacity_update_guard\",\n \"radroots_event_store_source_capacity_v1\",\n \"radroots_event_store_source_generation_capacity_advance\",\n \"radroots_event_store_source_generation_capacity_guard\"\n ],\n \"replaced_objects\": [\n \"radroots_event_store_food_availability_image_delete_guard\",\n \"radroots_event_store_food_availability_projection_delete_guard\",\n \"radroots_event_store_source_rebuild_marker_insert_guard\"\n ],\n \"tables\": [\n \"radroots_event_store_source_capacity_v1\"\n ],\n \"fts5_tables\": []\n }\n },\n \"source_maintenance\": {\n \"version\": 1,\n \"event_contract_registry_version\": 7,\n \"capacity_authority_id\": \"radroots_event_store_source_capacity_v1\",\n \"accounting\": {\n \"algorithm\": \"sqlite_cast_blob_octet_sum_v1\",\n \"raw_event_columns\": [\n \"event_id\",\n \"pubkey\",\n \"tags_json\",\n \"content\",\n \"sig\",\n \"raw_json\"\n ],\n \"raw_tag_columns\": [\n \"event_id\",\n \"tag_name\",\n \"tag_value\",\n \"tag_json\"\n ],\n \"nullable_raw_tag_columns\": [\n \"tag_value\"\n ]\n },\n \"limits\": {\n \"raw_events\": 25000,\n \"raw_tags\": 250000,\n \"raw_event_text_bytes\": 67108864,\n \"raw_tag_text_bytes\": 33554432,\n \"retained_source_generations\": 8\n },\n \"reopen_validation\": {\n \"mode\": \"bounded_full_raw_recount_v1\",\n \"raw_event_rejection_scan_bound\": 25001,\n \"raw_tag_rejection_scan_bound\": 250001,\n \"generation_history_validation\": \"bounded_count_plus_active_ordinal_v1\",\n \"retained_generation_rejection_scan_bound\": 9\n },\n \"rebuild_seal\": {\n \"nip09_hook_id\": \"nip09_reconciliation_v1\",\n \"nip09_manifest_sha256\": \"74af832420ffbaa9805e89df3c0b34f126a443e1598f757e3372f407f9003b77\",\n \"food_hook_id\": \"food_availability_projection_v1\",\n \"food_manifest_sha256\": \"02dfe1b450fbdac16e718888215b4dd5c85d8975440fa21e8f439fb24c2b2990\",\n \"food_scope_fingerprint_sha256\": \"8b63c5ddc48a2cc7db69295238b96d5f814dba50427c80b4d0079f061e6d3de0\",\n \"active_generation_authority\": \"radroots_event_store_source_state\",\n \"marker_close_authority\": \"radroots_event_store_source_capacity_marker_close_guard\"\n }\n },\n \"entry_points\": [\n {\n \"role\": \"migration_registry\",\n \"rust_path\": \"radroots_event_store::migrations::EVENT_STORE_MIGRATIONS[3]\"\n },\n {\n \"role\": \"migration_apply_hook\",\n \"rust_path\": \"radroots_event_store::schema::apply_migration_hook\"\n },\n {\n \"role\": \"migration_validation_hook\",\n \"rust_path\": \"radroots_event_store::schema::validate_migration_hook_state\"\n },\n {\n \"role\": \"capacity_query\",\n \"rust_path\": \"radroots_event_store::RadrootsEventStore::source_capacity_v1\"\n },\n {\n \"role\": \"raw_append_preflight\",\n \"rust_path\": \"radroots_event_store::source_maintenance_v1::preflight_unique_raw_source_append_v1\"\n },\n {\n \"role\": \"raw_append_advance\",\n \"rust_path\": \"radroots_event_store::source_maintenance_v1::advance_source_capacity_after_insert_v1\"\n },\n {\n \"role\": \"generation_append_preflight\",\n \"rust_path\": \"radroots_event_store::source_maintenance_v1::preflight_source_generation_append_v1\"\n },\n {\n \"role\": \"generation_rebuild_bind\",\n \"rust_path\": \"radroots_event_store::source_maintenance_v1::bind_source_capacity_to_generation_v1\"\n },\n {\n \"role\": \"sqlite_encoding_preflight\",\n \"rust_path\": \"radroots_event_store::store::validate_main_database_encoding\"\n },\n {\n \"role\": \"source_generation_history_rollback_guard\",\n \"rust_path\": \"radroots_event_store::schema::validate_rollback_preserves_source_generation_history\"\n },\n {\n \"role\": \"result_vector_executor\",\n \"rust_path\": \"source_maintenance_v1_result_vector\"\n }\n ],\n \"source_files\": [\n {\n \"role\": \"workspace_dependency_authority\",\n \"path\": \"Cargo.toml\",\n \"byte_length\": 12440,\n \"sha256\": \"8981f78d3ea85454a8cf7be156b3eef5659b399f1631d7c1068e7c336988070d\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"core_currency_value_authority\",\n \"path\": \"crates/core/src/currency.rs\",\n \"byte_length\": 4142,\n \"sha256\": \"42e6da2d8d2fdd6955dc2d83e98dd00266e02bc910b69923b117662c05089d14\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"core_decimal_value_authority\",\n \"path\": \"crates/core/src/decimal.rs\",\n \"byte_length\": 7839,\n \"sha256\": \"4b1d681a92d7a9e074bee7e1f20eb1499bb05bfc6528a6c9ba69fa7d00416550\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"core_money_value_authority\",\n \"path\": \"crates/core/src/money.rs\",\n \"byte_length\": 8025,\n \"sha256\": \"3f30bc21e21951a62fdc5d4033736ed42a883df590e8403940775bd2edfeebc5\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"core_percent_value_authority\",\n \"path\": \"crates/core/src/percent.rs\",\n \"byte_length\": 2652,\n \"sha256\": \"42ceab109881329f3539d06a0ee0381ab4359985fe185a18d58a9c9558c5eb7f\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"core_quantity_value_authority\",\n \"path\": \"crates/core/src/quantity.rs\",\n \"byte_length\": 6785,\n \"sha256\": \"d14e619da3829cdaf3ca7385fb892d330c286c1e21455c135c37518cbb588b5f\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"core_quantity_price_value_authority\",\n \"path\": \"crates/core/src/quantity_price.rs\",\n \"byte_length\": 7061,\n \"sha256\": \"d60f70377099470ed283f45868a5119d7bb46143cb250b5082a4c038ba7f6cdc\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"core_unit_value_authority\",\n \"path\": \"crates/core/src/unit.rs\",\n \"byte_length\": 10023,\n \"sha256\": \"550e9582b6a5290aac3f637b83853b21fefcbab92c432d067c83448f4b2fe3e9\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"blossom_public_surface\",\n \"path\": \"crates/blossom/src/lib.rs\",\n \"byte_length\": 510,\n \"sha256\": \"a4dfcbd193457c50c1b0fbfc91bb547981ecd3a6e23cfc6f1030edfb511dc58c\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"blossom_authorization_authority\",\n \"path\": \"crates/blossom/src/authorization.rs\",\n \"byte_length\": 39360,\n \"sha256\": \"461947b30516315a342b3b5697599f701d27050888f29f67f5d7a7ad4afa4c28\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"blossom_descriptor_authority\",\n \"path\": \"crates/blossom/src/descriptor.rs\",\n \"byte_length\": 13702,\n \"sha256\": \"b2a4ffa760256e1316f70e012d200e2b5f4afd8ede771b85a5147f96b247f599\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"blossom_error_authority\",\n \"path\": \"crates/blossom/src/error.rs\",\n \"byte_length\": 17229,\n \"sha256\": \"02af55beacf437040be17d1fc3271d6e2a62915f656ec2dbe06247deef83f95a\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"blossom_hash_authority\",\n \"path\": \"crates/blossom/src/hash.rs\",\n \"byte_length\": 11006,\n \"sha256\": \"45eca0d81ac0f46c305a32f81aafcdd90b14679d0614f72862289a6e5d4cb08f\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"blossom_media_type_authority\",\n \"path\": \"crates/blossom/src/media_type.rs\",\n \"byte_length\": 2368,\n \"sha256\": \"69db5c0f9fcdd7dbe1f22daa499f65603ae4ce198083dab2c73a36813fa18a46\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"blossom_url_authority\",\n \"path\": \"crates/blossom/src/url.rs\",\n \"byte_length\": 14580,\n \"sha256\": \"342c995fcf620e5fb32461a7d8276f5668ba2662890bd32d12780cc9121c6451\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_public_surface\",\n \"path\": \"crates/event/src/lib.rs\",\n \"byte_length\": 1892,\n \"sha256\": \"5d609b963a9b8db18ef96a10f394617413734d2631774d3296e6d8fc029cb02c\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_contract_facade\",\n \"path\": \"crates/event/src/contract.rs\",\n \"byte_length\": 208,\n \"sha256\": \"27466bf36461071931f391c60646a13f7781d19584583166cc54c0eece76d010\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_contract_registry_v7_authority\",\n \"path\": \"crates/event/src/contract/registry_v7.rs\",\n \"byte_length\": 145805,\n \"sha256\": \"0a62603f6fc05dc9f758561cf7da258c676ada810ceaeca7fb151364a5c83d62\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_envelope_authority\",\n \"path\": \"crates/event/src/envelope.rs\",\n \"byte_length\": 29310,\n \"sha256\": \"29f8d8c4b17c01cb5a8c7f59e4ce52e134180e0e6f37d265063d278b8f2ff26e\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_verification_typestate_authority\",\n \"path\": \"crates/event/src/verification.rs\",\n \"byte_length\": 9400,\n \"sha256\": \"417acb2ce670d266b1fa4fcafd6b48a910218deab38f57fb8b942056db76f94d\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_admission_typestate_authority\",\n \"path\": \"crates/event/src/admission.rs\",\n \"byte_length\": 7052,\n \"sha256\": \"62bd2ceb0f434946fdfc2b81f4efb13738676a29a6936c23a427e43aadf2d9cb\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_head_facade\",\n \"path\": \"crates/event/src/event_head.rs\",\n \"byte_length\": 97,\n \"sha256\": \"f761ff3e74c4f5e1e28381db00ce698c633ff048669b22984d14a587482e8e83\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_head_v1_authority\",\n \"path\": \"crates/event/src/event_head/v1.rs\",\n \"byte_length\": 6849,\n \"sha256\": \"9f4144d8d240023cf493ce0b538ade4c16dc44204d6b5f2797152caf6f9e7dba\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_ids_authority\",\n \"path\": \"crates/event/src/id.rs\",\n \"byte_length\": 48381,\n \"sha256\": \"a08fe3873815453a03318a706e2fa98a8a6728ec69d24368a87ec60025566fe0\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_trade_authority\",\n \"path\": \"crates/event/src/trade.rs\",\n \"byte_length\": 65549,\n \"sha256\": \"f818a7287ed82de3fb9ef1d973f0fb18d84b30eb4827ddc576ee502dd980145f\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_kinds_authority\",\n \"path\": \"crates/event/src/kinds.rs\",\n \"byte_length\": 34961,\n \"sha256\": \"8b3ce6193cab1f7e1587d0c1b2880a81b0aa43c77d89671b602772db37edcebf\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_tags_authority\",\n \"path\": \"crates/event/src/tags.rs\",\n \"byte_length\": 4530,\n \"sha256\": \"cb8f12a639fc72d9238e32495d9e67e928e6627fb8c94ec0a58c5d95e7373cd6\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_draft_authority\",\n \"path\": \"crates/event/src/draft.rs\",\n \"byte_length\": 51150,\n \"sha256\": \"ecc619aae28d688d13f35467398f17766b60f26c2bf3616d772ce65b7fa4fb53\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_calendar_authority\",\n \"path\": \"crates/event/src/calendar.rs\",\n \"byte_length\": 97992,\n \"sha256\": \"73e79d61ee175cb46e6123e1d28239252ddf0ad094dc1113bdb1286b63b88d57\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_classified_listing_authority\",\n \"path\": \"crates/event/src/classified_listing.rs\",\n \"byte_length\": 7956,\n \"sha256\": \"ac58484b87c7712c81f50f884d1c0dd60938571aa5f6f05d555191f529a4c2b0\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_profile_authority\",\n \"path\": \"crates/event/src/profile.rs\",\n \"byte_length\": 17011,\n \"sha256\": \"572bfe3b6f1711d109db445077f0a8453cad3f021cc0638f79201b618e16b978\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_post_authority\",\n \"path\": \"crates/event/src/post.rs\",\n \"byte_length\": 19968,\n \"sha256\": \"a822cd51eff9cbaa39db998cc5c90d117f7a983f0fe2d42d42118cb00e95982c\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_comment_authority\",\n \"path\": \"crates/event/src/comment.rs\",\n \"byte_length\": 34745,\n \"sha256\": \"24b7fda13229f21cee5cef2a9941e3aaafdbc486b19ed42731b0b6ee37c28703\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_food_availability_authority\",\n \"path\": \"crates/event/src/food_availability.rs\",\n \"byte_length\": 42829,\n \"sha256\": \"52472c07eb0855d884c60755dc3a3e1a963793451d707e51f769f2d78384f318\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_deletion_authority\",\n \"path\": \"crates/event/src/deletion.rs\",\n \"byte_length\": 30022,\n \"sha256\": \"8c3f2e32407520a8e8242c1293af32613c648fb76218b8003a18929ac0b3811f\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_dto_authority\",\n \"path\": \"crates/event/src/dto.rs\",\n \"byte_length\": 5038,\n \"sha256\": \"9e228c95ff6ff33f99441d8682781fd0a1c4d3f1db24284a43985dcbac3ba136\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_farm_crdt_authority\",\n \"path\": \"crates/event/src/farm_crdt.rs\",\n \"byte_length\": 21118,\n \"sha256\": \"cec265ac9c42b59a14be4eb96924d26a83477c27291f667f23e6e47a64b6b2c7\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_knowledge_authority\",\n \"path\": \"crates/event/src/knowledge.rs\",\n \"byte_length\": 54624,\n \"sha256\": \"3d55ff88a4e30d5d605f9ae4b2e96b9df6978d6c14e52f9aeb0b8039cfc200d7\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_operational_listing_authority\",\n \"path\": \"crates/event/src/operational_listing.rs\",\n \"byte_length\": 10307,\n \"sha256\": \"52e710db816c89c8d4b87c86541987f7447f2f7091c394c68d9cb98a27f90159\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_order_authority\",\n \"path\": \"crates/event/src/order.rs\",\n \"byte_length\": 60218,\n \"sha256\": \"4203fcc469612cac5c705884564e140b9fb076cd57079e22ed85275a8b936e50\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_reply_authority\",\n \"path\": \"crates/event/src/reply.rs\",\n \"byte_length\": 15581,\n \"sha256\": \"b0b83a1e96125c0892d932e9314519242a715fa8f46334aff765ffd248c91d9a\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_trade_validation_authority\",\n \"path\": \"crates/event/src/trade_validation.rs\",\n \"byte_length\": 4669,\n \"sha256\": \"1486f79bd1d0eadea6cc1b7646ce1f7f57ccade18334a3ea37286426676b9b98\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_relay_hint_authority\",\n \"path\": \"crates/event/src/relay_hint.rs\",\n \"byte_length\": 13598,\n \"sha256\": \"fb8b026eeccf51d78f20771c44223e7b48825aa0e3c760d6b3cb90148021a674\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_media_authority\",\n \"path\": \"crates/event/src/media.rs\",\n \"byte_length\": 3455,\n \"sha256\": \"10678af3a202d5367dbcf3bcdc0bf9fdf8d8fb89b045d7d3fb96b3cc195ab6f9\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_social_authority\",\n \"path\": \"crates/event/src/social.rs\",\n \"byte_length\": 6076,\n \"sha256\": \"a2dab19caad46eab14fac86e7c878a801a450d8ac374b0e728b71a5a224eef19\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_wire_facade\",\n \"path\": \"crates/event/src/wire.rs\",\n \"byte_length\": 68,\n \"sha256\": \"cb52f6006f7ecd862707d6b048f9fc407e0cd5ebcd3091b3c54e195ffa5cba64\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_wire_v1_authority\",\n \"path\": \"crates/event/src/wire/v1.rs\",\n \"byte_length\": 22198,\n \"sha256\": \"c818bc1f67e215948ba253c8c62b09ab788db9aeec9b6df8096014b55565cb8c\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_public_surface\",\n \"path\": \"crates/event_codec/src/lib.rs\",\n \"byte_length\": 2448,\n \"sha256\": \"7d4b0040ad3971f34395b17cb1aa40617e9c2c9e1b4222676c388c3adaa47e07\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_verification_facade\",\n \"path\": \"crates/event_codec/src/verification.rs\",\n \"byte_length\": 253,\n \"sha256\": \"b49a32df605035c87f295c0a151140d43d2e588c5e11b05183e5fc92993dae0e\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_verification_v1_authority\",\n \"path\": \"crates/event_codec/src/verification/v1.rs\",\n \"byte_length\": 6584,\n \"sha256\": \"72f64615bd6b6dc3b051579e3069b2213f3781dcc28e12f0b533c7d81b81b9b1\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_registry_v7_admission_authority\",\n \"path\": \"crates/event_codec/src/admission/registry_v7.rs\",\n \"byte_length\": 5228,\n \"sha256\": \"62da30cb6ef7d0ed2d43715a73bd529283e8ae8c12571bc5c68722cb400f17b8\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_admission_facade\",\n \"path\": \"crates/event_codec/src/admission.rs\",\n \"byte_length\": 21251,\n \"sha256\": \"25928e734e7b3688f92af806a12a13458c48fb47e94249f60f0226300d5da2b4\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_profile_inbound_facade\",\n \"path\": \"crates/event_codec/src/profile/inbound.rs\",\n \"byte_length\": 86,\n \"sha256\": \"9994cbca9dcc7450ad133b6b927e7c06bee08ef813652452cc1e56a284c1be10\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_profile_registry_v7_authority\",\n \"path\": \"crates/event_codec/src/profile/inbound/registry_v7.rs\",\n \"byte_length\": 10347,\n \"sha256\": \"32111c0e0592229c11a93a3a8054e7b124b039451aa696a9abcb56df2a6608d3\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_post_inbound_facade\",\n \"path\": \"crates/event_codec/src/post/inbound.rs\",\n \"byte_length\": 86,\n \"sha256\": \"9994cbca9dcc7450ad133b6b927e7c06bee08ef813652452cc1e56a284c1be10\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_post_registry_v7_authority\",\n \"path\": \"crates/event_codec/src/post/inbound/registry_v7.rs\",\n \"byte_length\": 15985,\n \"sha256\": \"e17083b0596e3a55994c399bc6272c0ebf04cabaa513260b3d45dd546f7f50ef\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_reply_inbound_facade\",\n \"path\": \"crates/event_codec/src/reply/inbound.rs\",\n \"byte_length\": 86,\n \"sha256\": \"9994cbca9dcc7450ad133b6b927e7c06bee08ef813652452cc1e56a284c1be10\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_reply_registry_v7_authority\",\n \"path\": \"crates/event_codec/src/reply/inbound/registry_v7.rs\",\n \"byte_length\": 26662,\n \"sha256\": \"d1d11116ca27801b7e2f17da60900e6a9ca0b6f27ea9ee0bf19e97dbf24c0c32\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_comment_inbound_facade\",\n \"path\": \"crates/event_codec/src/comment/inbound.rs\",\n \"byte_length\": 86,\n \"sha256\": \"9994cbca9dcc7450ad133b6b927e7c06bee08ef813652452cc1e56a284c1be10\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_comment_registry_v7_authority\",\n \"path\": \"crates/event_codec/src/comment/inbound/registry_v7.rs\",\n \"byte_length\": 53311,\n \"sha256\": \"c63fe853536b17bded6073f32b53434b60fc5123f911d01c642a6b3982f09c70\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_deletion_facade\",\n \"path\": \"crates/event_codec/src/deletion/mod.rs\",\n \"byte_length\": 115,\n \"sha256\": \"17d56f82ddb0a86bc97abbcf3e037fe460ea31fff2548f527be4bf004dac3a95\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_deletion_reconciliation_v1_authority\",\n \"path\": \"crates/event_codec/src/deletion/reconciliation_v1.rs\",\n \"byte_length\": 38028,\n \"sha256\": \"c6901f559e83700610595720c47a4d062078ae8411e1fc2fb952dd215b19dd90\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_error_authority\",\n \"path\": \"crates/event_codec/src/error.rs\",\n \"byte_length\": 3666,\n \"sha256\": \"174ce982bca37fac28d016c3ab3236441c9b31b2169ec94db6c9288b36c72849\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_food_admission_authority\",\n \"path\": \"crates/event_codec/src/food_availability/admission.rs\",\n \"byte_length\": 5681,\n \"sha256\": \"99809859090f5295572688d06bbb30dbe50524ba2c996de7ec5b26f6dd390643\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_food_authored_authority\",\n \"path\": \"crates/event_codec/src/food_availability/authored.rs\",\n \"byte_length\": 9724,\n \"sha256\": \"8673ec62ed3fc47b691efd9ca828643719803971e72b756c5b4603d409d0fba5\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_food_inbound_facade\",\n \"path\": \"crates/event_codec/src/food_availability/inbound.rs\",\n \"byte_length\": 86,\n \"sha256\": \"9994cbca9dcc7450ad133b6b927e7c06bee08ef813652452cc1e56a284c1be10\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_food_registry_v7_authority\",\n \"path\": \"crates/event_codec/src/food_availability/inbound/registry_v7.rs\",\n \"byte_length\": 26037,\n \"sha256\": \"92a7e75da3293967cec17a514479144c3c948a64542577dd58c7f07992968e4f\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_job_traits_authority\",\n \"path\": \"crates/event_codec/src/job/traits.rs\",\n \"byte_length\": 4946,\n \"sha256\": \"1b3558a3196744005978dceddd33d3a72ad1ab8f50fb1cbfd997dcfe98bb9e39\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_job_encode_authority\",\n \"path\": \"crates/event_codec/src/job/encode.rs\",\n \"byte_length\": 1670,\n \"sha256\": \"5d6cae6309fcdd02deb7c751b5c451d257cf1900986391cb39cbb2bfd1ddb577\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_knowledge_verification_authority\",\n \"path\": \"crates/event_codec/src/knowledge/verification.rs\",\n \"byte_length\": 6986,\n \"sha256\": \"6f8a10ed262ce37f05acd9c89a1e37ca2fa99ce7da18407891e8abe228ee2349\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_operational_listing_tags_authority\",\n \"path\": \"crates/event_codec/src/operational_listing/tags.rs\",\n \"byte_length\": 50222,\n \"sha256\": \"55b6860bc6f4699dd47a49f8336764e9db78aafb7c32849102dedeb96804b373\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_order_decode_authority\",\n \"path\": \"crates/event_codec/src/order/decode.rs\",\n \"byte_length\": 42667,\n \"sha256\": \"033d9465bc041d205eab3f820572b719a35651340dcb50bdce7c60fabee5f6e1\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_profile_facade\",\n \"path\": \"crates/event_codec/src/profile/mod.rs\",\n \"byte_length\": 1456,\n \"sha256\": \"06b890da54580e2ec68f7e729281b7c3b650ac0a95a4fcb0db3c6cdaa591a7ef\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_tag_builders_authority\",\n \"path\": \"crates/event_codec/src/tag_builders.rs\",\n \"byte_length\": 9232,\n \"sha256\": \"bd81ccf04fd72358331b48eddcde087a95e115c5bd90c05fd9332644979ad5e6\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_trade_facade\",\n \"path\": \"crates/event_codec/src/trade/mod.rs\",\n \"byte_length\": 21472,\n \"sha256\": \"10cee4b6cd6429ac9eb00327a521bcd0982fae4de98bda77e4728fda002965d3\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_store_dependency_authority\",\n \"path\": \"crates/event_store/Cargo.toml\",\n \"byte_length\": 1797,\n \"sha256\": \"e79f4b69500553835e2cde28e7d9139788bebe6e2cd4e0b4ae72cc150491daf4\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_store_error_and_limits\",\n \"path\": \"crates/event_store/src/error.rs\",\n \"byte_length\": 19458,\n \"sha256\": \"3d87df984af6ae5decf7ca8c3a8d5422be6c647e6a05490225b6b2037bf3701b\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"generated_descriptor_registration\",\n \"path\": \"crates/event_store/src/generated.rs\",\n \"byte_length\": 144,\n \"sha256\": \"6b0a8d6f249bd4fc3f878d37cb5e418680f0f1be2d9eec2518dedf03efc47121\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"public_surface\",\n \"path\": \"crates/event_store/src/lib.rs\",\n \"byte_length\": 3860,\n \"sha256\": \"de1c5cc1ab36e1166d23e2b76aeae1d5f0f401cf07ce243590741c0126d02132\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"migration_registry\",\n \"path\": \"crates/event_store/src/migrations.rs\",\n \"byte_length\": 73585,\n \"sha256\": \"a47477d04759ec6e71d14d3d05459864eae78fce0e3c2481a1b03147f04637f7\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"predecessor_model_public_surface\",\n \"path\": \"crates/event_store/src/model.rs\",\n \"byte_length\": 32070,\n \"sha256\": \"2cdbf6d1a4e0fa6680f45f02758513509339a7cf3a2387e824fb73b0edf749a4\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_store_reconciliation_v1_model\",\n \"path\": \"crates/event_store/src/model/reconciliation_v1.rs\",\n \"byte_length\": 10446,\n \"sha256\": \"475b7b840bffdae7e3f7a31f5b940b9dfa579eaa1cac640729aaaeadabf83ba6\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_store_reconciliation_v1_ingest_model\",\n \"path\": \"crates/event_store/src/model/ingest_reconciliation_v1.rs\",\n \"byte_length\": 1588,\n \"sha256\": \"6117e797674c35bb1ccebffbc6a8c0108bfc38c7c9f82607066dfd802b8f0734\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"addressable_transition_feed_model\",\n \"path\": \"crates/event_store/src/model/addressable_transition_feed_v1.rs\",\n \"byte_length\": 22229,\n \"sha256\": \"e651952d73ed0a29d2b137076f56f699beada205ac84ed8a96834e86e343fa78\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"current_visibility_model\",\n \"path\": \"crates/event_store/src/model/current_visibility_v1.rs\",\n \"byte_length\": 5634,\n \"sha256\": \"d64429bc7985231e923c1c3e5295d94270a817dd5dc96693f35dfdf2dc398e4f\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"food_projection_model\",\n \"path\": \"crates/event_store/src/model/food_availability_projection_v1.rs\",\n \"byte_length\": 17108,\n \"sha256\": \"67861a966d674efbc40f0b0433db2ec006048ff923f6261df4161e2ddba717b5\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"source_generation_rebuild_authority\",\n \"path\": \"crates/event_store/src/nip09/reconciliation_v1.rs\",\n \"byte_length\": 184833,\n \"sha256\": \"3ca802cb7a8444c8512b2be1390a019281704316a430fb9b75a8ad2cad358a07\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"nip09_successor_result_vector_executor\",\n \"path\": \"crates/event_store/tests/support/nip09_reconciliation_v1_result_vector_v2.rs\",\n \"byte_length\": 18205,\n \"sha256\": \"c632beee70dc777b8dd2a4f88cadb55296fee7aa5742e6e2c9f6d49f26ae2c78\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"schema_migration_and_reopen_authority\",\n \"path\": \"crates/event_store/src/schema.rs\",\n \"byte_length\": 146146,\n \"sha256\": \"93b060e80d3edd73f86208e4bf698fa9d53eaf1eeb04526c9261fb8b5726fb0d\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"public_store_and_transaction_authority\",\n \"path\": \"crates/event_store/src/store.rs\",\n \"byte_length\": 393218,\n \"sha256\": \"64565c3bc043779e1c2f26d9761901b109b4acab0f48c2021102c0e4e1f1f726\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"addressable_transition_feed_store\",\n \"path\": \"crates/event_store/src/store/addressable_transition_feed_v1.rs\",\n \"byte_length\": 40209,\n \"sha256\": \"2cebcca9602633652a79e7041d18d3a7d3feb1388a98f4f68137b9814efcedfa\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"current_visibility_store\",\n \"path\": \"crates/event_store/src/store/current_visibility_v1.rs\",\n \"byte_length\": 15737,\n \"sha256\": \"aa5d1bff7a5368cbac2b37906bb40f54bfc8aa6906bab34369569fdd105bf899\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"food_projection_store\",\n \"path\": \"crates/event_store/src/store/food_availability_projection_v1.rs\",\n \"byte_length\": 48919,\n \"sha256\": \"3a30da308ef863b806ed63d1cb65ac80f6a946a71821af84a0c595330491757e\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"predecessor_post_core_v1_extension\",\n \"path\": \"crates/event_store/src/store/post_core_extensions_v1.rs\",\n \"byte_length\": 6569,\n \"sha256\": \"f5bd8ddb45e1b2144895bd8da737fc92db051be1844ce7e8047a32b47376ce37\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"predecessor_post_core_v1_storage\",\n \"path\": \"crates/event_store/src/store/post_core_storage_v1.rs\",\n \"byte_length\": 16538,\n \"sha256\": \"fcd6546c23a6cba12b70d92728d19b6ee5d43174b83495f820348b77efa33aab\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"raw_ingest_capacity_authority\",\n \"path\": \"crates/event_store/src/store/protocol_reconciliation_v1.rs\",\n \"byte_length\": 29950,\n \"sha256\": \"c72aaff06e4f35d0b4523c6625e83345879f9e15b9fe9563dccf589e5d01e277\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"predecessor_protocol_storage\",\n \"path\": \"crates/event_store/src/store/protocol_storage_v1.rs\",\n \"byte_length\": 10835,\n \"sha256\": \"c82179b9f57968191f3e71dd008cc701787c62715ada74090eeecaa3b213076d\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"source_maintenance_runtime\",\n \"path\": \"crates/event_store/src/source_maintenance_v1.rs\",\n \"byte_length\": 51849,\n \"sha256\": \"db43df5849c811d767a87bfa7884ce499400d0b169734e18242542a4415b3a51\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"predecessor_food_result_vector_executor\",\n \"path\": \"crates/event_store/tests/food_availability_projection_v1_result_vector.rs\",\n \"byte_length\": 34046,\n \"sha256\": \"776903c6431ff07f26c6ad6b713db5628efce1a72f34d86de6e47d4404fa3a6d\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"artifact_transaction_authority\",\n \"path\": \"tools/xtask/src/contract/artifact_bundle.rs\",\n \"byte_length\": 39149,\n \"sha256\": \"3d1bba3980b80698d2a21e26caf7af6fe4d9dc4a7fbabc6e406b5d33928b457f\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"predecessor_successor_governance\",\n \"path\": \"tools/xtask/src/contract/food_availability_projection.rs\",\n \"byte_length\": 198991,\n \"sha256\": \"36fd3d8b5dda3b0855a06a47dae14cae7f4cc84aaca9902c31477c218859fccc\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"transitive_predecessor_membership_governance\",\n \"path\": \"tools/xtask/src/contract/nip09_reconciliation.rs\",\n \"byte_length\": 842433,\n \"sha256\": \"c5361d9da74ac6cbec1baa890818d5b00c935c5015c8e0cfaa9036a94fe2b69e\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"source_maintenance_governance\",\n \"path\": \"tools/xtask/src/contract/source_maintenance.rs\",\n \"byte_length\": 199270,\n \"sha256\": \"65b83a3ee1a700527d15c7403ca042cf2aaa4cd8935a2f8d2ab190c491a815bd\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"contract_command_authority\",\n \"path\": \"tools/xtask/src/contract.rs\",\n \"byte_length\": 508073,\n \"sha256\": \"26d1825a0d7a4a3bb983478f2ef87dc228cbfb68f794bb52864e34a62ef1eead\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"dto_root_generation_authority\",\n \"path\": \"tools/xtask/src/dto_roots.rs\",\n \"byte_length\": 35940,\n \"sha256\": \"a899a5695d7533c151d03477c4bd697a6e55c47a4980549724ac42046af694f5\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"xtask_dispatch_and_release_preflight\",\n \"path\": \"tools/xtask/src/main.rs\",\n \"byte_length\": 16426,\n \"sha256\": \"abc24896c6c740c10811a3e003ec4be8ebdb376a7b6b01445bfa7a5889f094a7\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n }\n ],\n \"public_api\": {\n \"inherited_predecessor_symbols\": [\n \"RADROOTS_ADDRESSABLE_TRANSITION_CURSOR_JSON_MAX_BYTES_V1\",\n \"RADROOTS_ADDRESSABLE_TRANSITION_D_TAG_MAX_BYTES_V1\",\n \"RADROOTS_ADDRESSABLE_TRANSITION_FEED_VERSION_V1\",\n \"RADROOTS_ADDRESSABLE_TRANSITION_PAGE_LIMIT_MAX_V1\",\n \"RADROOTS_ADDRESSABLE_TRANSITION_PAGE_RAW_JSON_MAX_BYTES_V1\",\n \"RADROOTS_ADDRESSABLE_TRANSITION_PAGE_SCAN_MAX_V1\",\n \"RADROOTS_ADDRESSABLE_TRANSITION_SCOPE_KIND_MAX_V1\",\n \"RADROOTS_FOOD_AVAILABILITY_PROJECTION_APPLY_PAGE_LIMIT_V1\",\n \"RADROOTS_FOOD_AVAILABILITY_PROJECTION_VERSION_V1\",\n \"RADROOTS_FOOD_AVAILABILITY_SEARCH_QUERY_MAX_BYTES_V1\",\n \"RADROOTS_FOOD_AVAILABILITY_SEARCH_QUERY_MAX_TERMS_V1\",\n \"RadrootsAddressableTransitionCauseV1\",\n \"RadrootsAddressableTransitionCoordinateV1\",\n \"RadrootsAddressableTransitionCursorV1\",\n \"RadrootsAddressableTransitionEventReferenceV1\",\n \"RadrootsAddressableTransitionOriginV1\",\n \"RadrootsAddressableTransitionPageV1\",\n \"RadrootsAddressableTransitionRawHeadDecisionV1\",\n \"RadrootsAddressableTransitionScopeFingerprintV1\",\n \"RadrootsAddressableTransitionScopeV1\",\n \"RadrootsAddressableTransitionV1\",\n \"RadrootsAddressableTransitionVisibilityV1\",\n \"RadrootsCurrentEventVisibilityV1\",\n \"RadrootsCurrentVisibilityDecisionV1\",\n \"RadrootsFoodAvailabilitySearchQueryV1\",\n \"RadrootsFoodAvailabilityStatusFilterV1\",\n \"RadrootsNip09SuppressionEvidenceV1\",\n \"RadrootsNip09SuppressionOutcome\",\n \"RadrootsNip09SuppressionReason\",\n \"RadrootsStoreProducedCanonicalEventV1\",\n \"RadrootsStoredFoodAvailabilityImageV1\",\n \"RadrootsStoredFoodAvailabilityV1\"\n ],\n \"added_symbols\": [\n \"RADROOTS_EVENT_STORE_RAW_EVENT_COUNT_LIMIT_V1\",\n \"RADROOTS_EVENT_STORE_RAW_EVENT_TEXT_BYTES_LIMIT_V1\",\n \"RADROOTS_EVENT_STORE_RAW_TAG_COUNT_LIMIT_V1\",\n \"RADROOTS_EVENT_STORE_RAW_TAG_TEXT_BYTES_LIMIT_V1\",\n \"RADROOTS_EVENT_STORE_RETAINED_SOURCE_GENERATION_LIMIT_V1\",\n \"RadrootsEventStoreSourceCapacityResourceV1\",\n \"RadrootsEventStoreSourceCapacityV1\"\n ],\n \"methods\": [\n \"RadrootsEventStore::source_capacity_v1\",\n \"RadrootsEventStoreSourceCapacityResourceV1::as_str\",\n \"RadrootsEventStoreSourceCapacityV1::source_generation\",\n \"RadrootsEventStoreSourceCapacityV1::raw_event_count\",\n \"RadrootsEventStoreSourceCapacityV1::raw_tag_count\",\n \"RadrootsEventStoreSourceCapacityV1::raw_event_text_bytes\",\n \"RadrootsEventStoreSourceCapacityV1::raw_tag_text_bytes\",\n \"RadrootsEventStoreSourceCapacityV1::raw_high_water_seq\",\n \"RadrootsEventStoreSourceCapacityV1::retained_generation_count\",\n \"RadrootsEventStoreSourceCapacityV1::retained_generation_limit\"\n ],\n \"error_variants\": [\n \"SourceCapacityExceeded\",\n \"SourceGenerationHistoryLimitReached\",\n \"PersistedEphemeralRawEvent\",\n \"SourceCapacityStateDrift\",\n \"SqliteMainDatabaseEncodingNotUtf8\",\n \"RollbackWouldDiscardSourceGenerationHistory\"\n ],\n \"removed_symbols\": [\n \"RadrootsEventStoreReconciliationResource\",\n \"RadrootsEventStoreError::ReconciliationCapacityExceeded\"\n ],\n \"breaking_replacements\": [\n {\n \"removed\": \"RadrootsEventStoreReconciliationResource\",\n \"replacement\": \"RadrootsEventStoreSourceCapacityResourceV1\"\n },\n {\n \"removed\": \"RadrootsEventStoreError::ReconciliationCapacityExceeded\",\n \"replacement\": \"RadrootsEventStoreError::SourceCapacityExceeded\"\n }\n ]\n },\n \"result_vector\": {\n \"canonical_path\": \"contracts/conformance/vectors/event_store/source_maintenance.v1.json\",\n \"mirror_path\": \"crates/event_store/tests/fixtures/source_maintenance.v1.json\",\n \"byte_length\": 16253,\n \"sha256\": \"997aba2604a2b9d199fb87dc9d07942ca50d91863aeadcf3eeacf16d191dd71f\",\n \"hash_algorithm\": \"sha256_bytes_v1\",\n \"executor_id\": \"radroots_event_store.source_maintenance_v1.result_vector_executor.v1\",\n \"executor_path\": \"crates/event_store/tests/source_maintenance_v1_result_vector.rs\",\n \"executor_test\": \"source_maintenance_v1_result_vector\",\n \"executor_byte_length\": 23510,\n \"executor_sha256\": \"a7487afdfe19fc5fc794811d0f0e6035203e1aabcf0a33a1d398f6b3555d38f3\",\n \"executor_hash_algorithm\": \"sha256_bytes_v1\"\n }\n}\n";
+pub(crate) const SOURCE_MAINTENANCE_MANIFEST_JSON: &str = "{\n \"schema_version\": 1,\n \"contract_id\": \"radroots_event_store.source_maintenance_v1\",\n \"hook_id\": \"source_maintenance_v1\",\n \"manifest_schema\": {\n \"path\": \"crates/event_store/contracts/source_maintenance_v1.manifest.schema.json\",\n \"byte_length\": 12315,\n \"sha256\": \"96ea98a5abf3348b61565954cd34f2c622411d8dd34205ef04134bbe0e3d3f2b\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n \"predecessor\": {\n \"hook_id\": \"food_availability_projection_v1\",\n \"manifest\": {\n \"path\": \"crates/event_store/contracts/food_availability_projection_v1.manifest.json\",\n \"byte_length\": 17455,\n \"sha256\": \"02dfe1b450fbdac16e718888215b4dd5c85d8975440fa21e8f439fb24c2b2990\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n }\n },\n \"migration\": {\n \"version\": 4,\n \"name\": \"source_maintenance\",\n \"up\": {\n \"path\": \"crates/event_store/migrations/0004_source_maintenance.up.sql\",\n \"byte_length\": 19841,\n \"sha256\": \"ab2724188f8d08c897eebea2533a635e7c74282a25e84e4c0c37e78b08837a43\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n \"down\": {\n \"path\": \"crates/event_store/migrations/0004_source_maintenance.down.sql\",\n \"byte_length\": 5172,\n \"sha256\": \"fe44fd53c51545c08ea479b385e6781079dab70fc63da2a3c205d727a00ce860\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n \"schema_sha256\": \"074f85b663444ac150239ecd8441ea4a96ad83a798a55e22d2e5e2f7ee943a8c\",\n \"catalog\": {\n \"objects\": [\n \"radroots_event_store_source_capacity_delete_guard\",\n \"radroots_event_store_source_capacity_insert_guard\",\n \"radroots_event_store_source_capacity_marker_close_guard\",\n \"radroots_event_store_source_capacity_update_guard\",\n \"radroots_event_store_source_capacity_v1\",\n \"radroots_event_store_source_generation_capacity_advance\",\n \"radroots_event_store_source_generation_capacity_guard\"\n ],\n \"replaced_objects\": [\n \"radroots_event_store_food_availability_image_delete_guard\",\n \"radroots_event_store_food_availability_projection_delete_guard\",\n \"radroots_event_store_source_rebuild_marker_insert_guard\"\n ],\n \"tables\": [\n \"radroots_event_store_source_capacity_v1\"\n ],\n \"fts5_tables\": []\n }\n },\n \"source_maintenance\": {\n \"version\": 1,\n \"event_contract_registry_version\": 7,\n \"capacity_authority_id\": \"radroots_event_store_source_capacity_v1\",\n \"accounting\": {\n \"algorithm\": \"sqlite_cast_blob_octet_sum_v1\",\n \"raw_event_columns\": [\n \"event_id\",\n \"pubkey\",\n \"tags_json\",\n \"content\",\n \"sig\",\n \"raw_json\"\n ],\n \"raw_tag_columns\": [\n \"event_id\",\n \"tag_name\",\n \"tag_value\",\n \"tag_json\"\n ],\n \"nullable_raw_tag_columns\": [\n \"tag_value\"\n ]\n },\n \"limits\": {\n \"raw_events\": 25000,\n \"raw_tags\": 250000,\n \"raw_event_text_bytes\": 67108864,\n \"raw_tag_text_bytes\": 33554432,\n \"retained_source_generations\": 8\n },\n \"reopen_validation\": {\n \"mode\": \"bounded_full_raw_recount_v1\",\n \"raw_event_rejection_scan_bound\": 25001,\n \"raw_tag_rejection_scan_bound\": 250001,\n \"generation_history_validation\": \"bounded_count_plus_active_ordinal_v1\",\n \"retained_generation_rejection_scan_bound\": 9\n },\n \"rebuild_seal\": {\n \"nip09_hook_id\": \"nip09_reconciliation_v1\",\n \"nip09_manifest_sha256\": \"74af832420ffbaa9805e89df3c0b34f126a443e1598f757e3372f407f9003b77\",\n \"food_hook_id\": \"food_availability_projection_v1\",\n \"food_manifest_sha256\": \"02dfe1b450fbdac16e718888215b4dd5c85d8975440fa21e8f439fb24c2b2990\",\n \"food_scope_fingerprint_sha256\": \"8b63c5ddc48a2cc7db69295238b96d5f814dba50427c80b4d0079f061e6d3de0\",\n \"active_generation_authority\": \"radroots_event_store_source_state\",\n \"marker_close_authority\": \"radroots_event_store_source_capacity_marker_close_guard\"\n }\n },\n \"entry_points\": [\n {\n \"role\": \"migration_registry\",\n \"rust_path\": \"radroots_event_store::migrations::EVENT_STORE_MIGRATIONS[3]\"\n },\n {\n \"role\": \"migration_apply_hook\",\n \"rust_path\": \"radroots_event_store::schema::apply_migration_hook\"\n },\n {\n \"role\": \"migration_validation_hook\",\n \"rust_path\": \"radroots_event_store::schema::validate_migration_hook_state\"\n },\n {\n \"role\": \"capacity_query\",\n \"rust_path\": \"radroots_event_store::RadrootsEventStore::source_capacity_v1\"\n },\n {\n \"role\": \"raw_append_preflight\",\n \"rust_path\": \"radroots_event_store::source_maintenance_v1::preflight_unique_raw_source_append_v1\"\n },\n {\n \"role\": \"raw_append_advance\",\n \"rust_path\": \"radroots_event_store::source_maintenance_v1::advance_source_capacity_after_insert_v1\"\n },\n {\n \"role\": \"generation_append_preflight\",\n \"rust_path\": \"radroots_event_store::source_maintenance_v1::preflight_source_generation_append_v1\"\n },\n {\n \"role\": \"generation_rebuild_bind\",\n \"rust_path\": \"radroots_event_store::source_maintenance_v1::bind_source_capacity_to_generation_v1\"\n },\n {\n \"role\": \"sqlite_encoding_preflight\",\n \"rust_path\": \"radroots_event_store::store::validate_main_database_encoding\"\n },\n {\n \"role\": \"source_generation_history_rollback_guard\",\n \"rust_path\": \"radroots_event_store::schema::validate_rollback_preserves_source_generation_history\"\n },\n {\n \"role\": \"result_vector_executor\",\n \"rust_path\": \"source_maintenance_v1_result_vector\"\n }\n ],\n \"source_files\": [\n {\n \"role\": \"workspace_dependency_authority\",\n \"path\": \"Cargo.toml\",\n \"byte_length\": 12440,\n \"sha256\": \"8981f78d3ea85454a8cf7be156b3eef5659b399f1631d7c1068e7c336988070d\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"core_currency_value_authority\",\n \"path\": \"crates/core/src/currency.rs\",\n \"byte_length\": 4142,\n \"sha256\": \"42e6da2d8d2fdd6955dc2d83e98dd00266e02bc910b69923b117662c05089d14\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"core_decimal_value_authority\",\n \"path\": \"crates/core/src/decimal.rs\",\n \"byte_length\": 7839,\n \"sha256\": \"4b1d681a92d7a9e074bee7e1f20eb1499bb05bfc6528a6c9ba69fa7d00416550\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"core_money_value_authority\",\n \"path\": \"crates/core/src/money.rs\",\n \"byte_length\": 8025,\n \"sha256\": \"3f30bc21e21951a62fdc5d4033736ed42a883df590e8403940775bd2edfeebc5\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"core_percent_value_authority\",\n \"path\": \"crates/core/src/percent.rs\",\n \"byte_length\": 2652,\n \"sha256\": \"42ceab109881329f3539d06a0ee0381ab4359985fe185a18d58a9c9558c5eb7f\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"core_quantity_value_authority\",\n \"path\": \"crates/core/src/quantity.rs\",\n \"byte_length\": 6785,\n \"sha256\": \"d14e619da3829cdaf3ca7385fb892d330c286c1e21455c135c37518cbb588b5f\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"core_quantity_price_value_authority\",\n \"path\": \"crates/core/src/quantity_price.rs\",\n \"byte_length\": 7061,\n \"sha256\": \"d60f70377099470ed283f45868a5119d7bb46143cb250b5082a4c038ba7f6cdc\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"core_unit_value_authority\",\n \"path\": \"crates/core/src/unit.rs\",\n \"byte_length\": 10023,\n \"sha256\": \"550e9582b6a5290aac3f637b83853b21fefcbab92c432d067c83448f4b2fe3e9\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"blossom_public_surface\",\n \"path\": \"crates/blossom/src/lib.rs\",\n \"byte_length\": 510,\n \"sha256\": \"a4dfcbd193457c50c1b0fbfc91bb547981ecd3a6e23cfc6f1030edfb511dc58c\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"blossom_authorization_authority\",\n \"path\": \"crates/blossom/src/authorization.rs\",\n \"byte_length\": 39360,\n \"sha256\": \"461947b30516315a342b3b5697599f701d27050888f29f67f5d7a7ad4afa4c28\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"blossom_descriptor_authority\",\n \"path\": \"crates/blossom/src/descriptor.rs\",\n \"byte_length\": 13702,\n \"sha256\": \"b2a4ffa760256e1316f70e012d200e2b5f4afd8ede771b85a5147f96b247f599\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"blossom_error_authority\",\n \"path\": \"crates/blossom/src/error.rs\",\n \"byte_length\": 17229,\n \"sha256\": \"02af55beacf437040be17d1fc3271d6e2a62915f656ec2dbe06247deef83f95a\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"blossom_hash_authority\",\n \"path\": \"crates/blossom/src/hash.rs\",\n \"byte_length\": 11006,\n \"sha256\": \"45eca0d81ac0f46c305a32f81aafcdd90b14679d0614f72862289a6e5d4cb08f\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"blossom_media_type_authority\",\n \"path\": \"crates/blossom/src/media_type.rs\",\n \"byte_length\": 2368,\n \"sha256\": \"69db5c0f9fcdd7dbe1f22daa499f65603ae4ce198083dab2c73a36813fa18a46\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"blossom_url_authority\",\n \"path\": \"crates/blossom/src/url.rs\",\n \"byte_length\": 14580,\n \"sha256\": \"342c995fcf620e5fb32461a7d8276f5668ba2662890bd32d12780cc9121c6451\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_public_surface\",\n \"path\": \"crates/event/src/lib.rs\",\n \"byte_length\": 1892,\n \"sha256\": \"5d609b963a9b8db18ef96a10f394617413734d2631774d3296e6d8fc029cb02c\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_contract_facade\",\n \"path\": \"crates/event/src/contract.rs\",\n \"byte_length\": 208,\n \"sha256\": \"27466bf36461071931f391c60646a13f7781d19584583166cc54c0eece76d010\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_contract_registry_v7_authority\",\n \"path\": \"crates/event/src/contract/registry_v7.rs\",\n \"byte_length\": 145805,\n \"sha256\": \"0a62603f6fc05dc9f758561cf7da258c676ada810ceaeca7fb151364a5c83d62\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_envelope_authority\",\n \"path\": \"crates/event/src/envelope.rs\",\n \"byte_length\": 29310,\n \"sha256\": \"29f8d8c4b17c01cb5a8c7f59e4ce52e134180e0e6f37d265063d278b8f2ff26e\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_verification_typestate_authority\",\n \"path\": \"crates/event/src/verification.rs\",\n \"byte_length\": 9400,\n \"sha256\": \"417acb2ce670d266b1fa4fcafd6b48a910218deab38f57fb8b942056db76f94d\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_admission_typestate_authority\",\n \"path\": \"crates/event/src/admission.rs\",\n \"byte_length\": 7052,\n \"sha256\": \"62bd2ceb0f434946fdfc2b81f4efb13738676a29a6936c23a427e43aadf2d9cb\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_head_facade\",\n \"path\": \"crates/event/src/event_head.rs\",\n \"byte_length\": 97,\n \"sha256\": \"f761ff3e74c4f5e1e28381db00ce698c633ff048669b22984d14a587482e8e83\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_head_v1_authority\",\n \"path\": \"crates/event/src/event_head/v1.rs\",\n \"byte_length\": 6849,\n \"sha256\": \"9f4144d8d240023cf493ce0b538ade4c16dc44204d6b5f2797152caf6f9e7dba\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_ids_authority\",\n \"path\": \"crates/event/src/id.rs\",\n \"byte_length\": 48381,\n \"sha256\": \"a08fe3873815453a03318a706e2fa98a8a6728ec69d24368a87ec60025566fe0\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_trade_authority\",\n \"path\": \"crates/event/src/trade.rs\",\n \"byte_length\": 65549,\n \"sha256\": \"f818a7287ed82de3fb9ef1d973f0fb18d84b30eb4827ddc576ee502dd980145f\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_kinds_authority\",\n \"path\": \"crates/event/src/kinds.rs\",\n \"byte_length\": 34961,\n \"sha256\": \"8b3ce6193cab1f7e1587d0c1b2880a81b0aa43c77d89671b602772db37edcebf\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_tags_authority\",\n \"path\": \"crates/event/src/tags.rs\",\n \"byte_length\": 4530,\n \"sha256\": \"cb8f12a639fc72d9238e32495d9e67e928e6627fb8c94ec0a58c5d95e7373cd6\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_draft_authority\",\n \"path\": \"crates/event/src/draft.rs\",\n \"byte_length\": 51150,\n \"sha256\": \"ecc619aae28d688d13f35467398f17766b60f26c2bf3616d772ce65b7fa4fb53\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_calendar_authority\",\n \"path\": \"crates/event/src/calendar.rs\",\n \"byte_length\": 97992,\n \"sha256\": \"73e79d61ee175cb46e6123e1d28239252ddf0ad094dc1113bdb1286b63b88d57\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_classified_listing_authority\",\n \"path\": \"crates/event/src/classified_listing.rs\",\n \"byte_length\": 7956,\n \"sha256\": \"ac58484b87c7712c81f50f884d1c0dd60938571aa5f6f05d555191f529a4c2b0\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_profile_authority\",\n \"path\": \"crates/event/src/profile.rs\",\n \"byte_length\": 17011,\n \"sha256\": \"572bfe3b6f1711d109db445077f0a8453cad3f021cc0638f79201b618e16b978\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_post_authority\",\n \"path\": \"crates/event/src/post.rs\",\n \"byte_length\": 19968,\n \"sha256\": \"a822cd51eff9cbaa39db998cc5c90d117f7a983f0fe2d42d42118cb00e95982c\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_comment_authority\",\n \"path\": \"crates/event/src/comment.rs\",\n \"byte_length\": 34745,\n \"sha256\": \"24b7fda13229f21cee5cef2a9941e3aaafdbc486b19ed42731b0b6ee37c28703\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_food_availability_authority\",\n \"path\": \"crates/event/src/food_availability.rs\",\n \"byte_length\": 42829,\n \"sha256\": \"52472c07eb0855d884c60755dc3a3e1a963793451d707e51f769f2d78384f318\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_deletion_authority\",\n \"path\": \"crates/event/src/deletion.rs\",\n \"byte_length\": 30022,\n \"sha256\": \"8c3f2e32407520a8e8242c1293af32613c648fb76218b8003a18929ac0b3811f\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_dto_authority\",\n \"path\": \"crates/event/src/dto.rs\",\n \"byte_length\": 5038,\n \"sha256\": \"9e228c95ff6ff33f99441d8682781fd0a1c4d3f1db24284a43985dcbac3ba136\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_farm_crdt_authority\",\n \"path\": \"crates/event/src/farm_crdt.rs\",\n \"byte_length\": 21118,\n \"sha256\": \"cec265ac9c42b59a14be4eb96924d26a83477c27291f667f23e6e47a64b6b2c7\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_knowledge_authority\",\n \"path\": \"crates/event/src/knowledge.rs\",\n \"byte_length\": 54624,\n \"sha256\": \"3d55ff88a4e30d5d605f9ae4b2e96b9df6978d6c14e52f9aeb0b8039cfc200d7\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_operational_listing_authority\",\n \"path\": \"crates/event/src/operational_listing.rs\",\n \"byte_length\": 10307,\n \"sha256\": \"52e710db816c89c8d4b87c86541987f7447f2f7091c394c68d9cb98a27f90159\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_order_authority\",\n \"path\": \"crates/event/src/order.rs\",\n \"byte_length\": 60218,\n \"sha256\": \"4203fcc469612cac5c705884564e140b9fb076cd57079e22ed85275a8b936e50\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_reply_authority\",\n \"path\": \"crates/event/src/reply.rs\",\n \"byte_length\": 15581,\n \"sha256\": \"b0b83a1e96125c0892d932e9314519242a715fa8f46334aff765ffd248c91d9a\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_trade_validation_authority\",\n \"path\": \"crates/event/src/trade_validation.rs\",\n \"byte_length\": 4669,\n \"sha256\": \"1486f79bd1d0eadea6cc1b7646ce1f7f57ccade18334a3ea37286426676b9b98\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_relay_hint_authority\",\n \"path\": \"crates/event/src/relay_hint.rs\",\n \"byte_length\": 13598,\n \"sha256\": \"fb8b026eeccf51d78f20771c44223e7b48825aa0e3c760d6b3cb90148021a674\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_media_authority\",\n \"path\": \"crates/event/src/media.rs\",\n \"byte_length\": 3455,\n \"sha256\": \"10678af3a202d5367dbcf3bcdc0bf9fdf8d8fb89b045d7d3fb96b3cc195ab6f9\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_social_authority\",\n \"path\": \"crates/event/src/social.rs\",\n \"byte_length\": 6076,\n \"sha256\": \"a2dab19caad46eab14fac86e7c878a801a450d8ac374b0e728b71a5a224eef19\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_wire_facade\",\n \"path\": \"crates/event/src/wire.rs\",\n \"byte_length\": 68,\n \"sha256\": \"cb52f6006f7ecd862707d6b048f9fc407e0cd5ebcd3091b3c54e195ffa5cba64\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_wire_v1_authority\",\n \"path\": \"crates/event/src/wire/v1.rs\",\n \"byte_length\": 22198,\n \"sha256\": \"c818bc1f67e215948ba253c8c62b09ab788db9aeec9b6df8096014b55565cb8c\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_public_surface\",\n \"path\": \"crates/event_codec/src/lib.rs\",\n \"byte_length\": 2448,\n \"sha256\": \"7d4b0040ad3971f34395b17cb1aa40617e9c2c9e1b4222676c388c3adaa47e07\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_verification_facade\",\n \"path\": \"crates/event_codec/src/verification.rs\",\n \"byte_length\": 253,\n \"sha256\": \"b49a32df605035c87f295c0a151140d43d2e588c5e11b05183e5fc92993dae0e\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_verification_v1_authority\",\n \"path\": \"crates/event_codec/src/verification/v1.rs\",\n \"byte_length\": 6584,\n \"sha256\": \"72f64615bd6b6dc3b051579e3069b2213f3781dcc28e12f0b533c7d81b81b9b1\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_registry_v7_admission_authority\",\n \"path\": \"crates/event_codec/src/admission/registry_v7.rs\",\n \"byte_length\": 5228,\n \"sha256\": \"62da30cb6ef7d0ed2d43715a73bd529283e8ae8c12571bc5c68722cb400f17b8\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_admission_facade\",\n \"path\": \"crates/event_codec/src/admission.rs\",\n \"byte_length\": 21251,\n \"sha256\": \"25928e734e7b3688f92af806a12a13458c48fb47e94249f60f0226300d5da2b4\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_profile_inbound_facade\",\n \"path\": \"crates/event_codec/src/profile/inbound.rs\",\n \"byte_length\": 86,\n \"sha256\": \"9994cbca9dcc7450ad133b6b927e7c06bee08ef813652452cc1e56a284c1be10\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_profile_registry_v7_authority\",\n \"path\": \"crates/event_codec/src/profile/inbound/registry_v7.rs\",\n \"byte_length\": 10347,\n \"sha256\": \"32111c0e0592229c11a93a3a8054e7b124b039451aa696a9abcb56df2a6608d3\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_post_inbound_facade\",\n \"path\": \"crates/event_codec/src/post/inbound.rs\",\n \"byte_length\": 86,\n \"sha256\": \"9994cbca9dcc7450ad133b6b927e7c06bee08ef813652452cc1e56a284c1be10\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_post_registry_v7_authority\",\n \"path\": \"crates/event_codec/src/post/inbound/registry_v7.rs\",\n \"byte_length\": 15985,\n \"sha256\": \"e17083b0596e3a55994c399bc6272c0ebf04cabaa513260b3d45dd546f7f50ef\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_reply_inbound_facade\",\n \"path\": \"crates/event_codec/src/reply/inbound.rs\",\n \"byte_length\": 86,\n \"sha256\": \"9994cbca9dcc7450ad133b6b927e7c06bee08ef813652452cc1e56a284c1be10\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_reply_registry_v7_authority\",\n \"path\": \"crates/event_codec/src/reply/inbound/registry_v7.rs\",\n \"byte_length\": 26662,\n \"sha256\": \"d1d11116ca27801b7e2f17da60900e6a9ca0b6f27ea9ee0bf19e97dbf24c0c32\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_comment_inbound_facade\",\n \"path\": \"crates/event_codec/src/comment/inbound.rs\",\n \"byte_length\": 86,\n \"sha256\": \"9994cbca9dcc7450ad133b6b927e7c06bee08ef813652452cc1e56a284c1be10\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_comment_registry_v7_authority\",\n \"path\": \"crates/event_codec/src/comment/inbound/registry_v7.rs\",\n \"byte_length\": 53311,\n \"sha256\": \"c63fe853536b17bded6073f32b53434b60fc5123f911d01c642a6b3982f09c70\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_deletion_facade\",\n \"path\": \"crates/event_codec/src/deletion/mod.rs\",\n \"byte_length\": 115,\n \"sha256\": \"17d56f82ddb0a86bc97abbcf3e037fe460ea31fff2548f527be4bf004dac3a95\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_deletion_reconciliation_v1_authority\",\n \"path\": \"crates/event_codec/src/deletion/reconciliation_v1.rs\",\n \"byte_length\": 38028,\n \"sha256\": \"c6901f559e83700610595720c47a4d062078ae8411e1fc2fb952dd215b19dd90\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_error_authority\",\n \"path\": \"crates/event_codec/src/error.rs\",\n \"byte_length\": 3666,\n \"sha256\": \"174ce982bca37fac28d016c3ab3236441c9b31b2169ec94db6c9288b36c72849\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_food_admission_authority\",\n \"path\": \"crates/event_codec/src/food_availability/admission.rs\",\n \"byte_length\": 5681,\n \"sha256\": \"99809859090f5295572688d06bbb30dbe50524ba2c996de7ec5b26f6dd390643\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_food_authored_authority\",\n \"path\": \"crates/event_codec/src/food_availability/authored.rs\",\n \"byte_length\": 9724,\n \"sha256\": \"8673ec62ed3fc47b691efd9ca828643719803971e72b756c5b4603d409d0fba5\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_food_inbound_facade\",\n \"path\": \"crates/event_codec/src/food_availability/inbound.rs\",\n \"byte_length\": 86,\n \"sha256\": \"9994cbca9dcc7450ad133b6b927e7c06bee08ef813652452cc1e56a284c1be10\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_food_registry_v7_authority\",\n \"path\": \"crates/event_codec/src/food_availability/inbound/registry_v7.rs\",\n \"byte_length\": 26037,\n \"sha256\": \"92a7e75da3293967cec17a514479144c3c948a64542577dd58c7f07992968e4f\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_job_traits_authority\",\n \"path\": \"crates/event_codec/src/job/traits.rs\",\n \"byte_length\": 4946,\n \"sha256\": \"1b3558a3196744005978dceddd33d3a72ad1ab8f50fb1cbfd997dcfe98bb9e39\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_job_encode_authority\",\n \"path\": \"crates/event_codec/src/job/encode.rs\",\n \"byte_length\": 1670,\n \"sha256\": \"5d6cae6309fcdd02deb7c751b5c451d257cf1900986391cb39cbb2bfd1ddb577\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_knowledge_verification_authority\",\n \"path\": \"crates/event_codec/src/knowledge/verification.rs\",\n \"byte_length\": 6986,\n \"sha256\": \"6f8a10ed262ce37f05acd9c89a1e37ca2fa99ce7da18407891e8abe228ee2349\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_operational_listing_tags_authority\",\n \"path\": \"crates/event_codec/src/operational_listing/tags.rs\",\n \"byte_length\": 50222,\n \"sha256\": \"55b6860bc6f4699dd47a49f8336764e9db78aafb7c32849102dedeb96804b373\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_order_decode_authority\",\n \"path\": \"crates/event_codec/src/order/decode.rs\",\n \"byte_length\": 42667,\n \"sha256\": \"033d9465bc041d205eab3f820572b719a35651340dcb50bdce7c60fabee5f6e1\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_profile_facade\",\n \"path\": \"crates/event_codec/src/profile/mod.rs\",\n \"byte_length\": 1456,\n \"sha256\": \"06b890da54580e2ec68f7e729281b7c3b650ac0a95a4fcb0db3c6cdaa591a7ef\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_tag_builders_authority\",\n \"path\": \"crates/event_codec/src/tag_builders.rs\",\n \"byte_length\": 9232,\n \"sha256\": \"bd81ccf04fd72358331b48eddcde087a95e115c5bd90c05fd9332644979ad5e6\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_codec_trade_facade\",\n \"path\": \"crates/event_codec/src/trade/mod.rs\",\n \"byte_length\": 21472,\n \"sha256\": \"10cee4b6cd6429ac9eb00327a521bcd0982fae4de98bda77e4728fda002965d3\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_store_dependency_authority\",\n \"path\": \"crates/event_store/Cargo.toml\",\n \"byte_length\": 1797,\n \"sha256\": \"e79f4b69500553835e2cde28e7d9139788bebe6e2cd4e0b4ae72cc150491daf4\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_store_error_and_limits\",\n \"path\": \"crates/event_store/src/error.rs\",\n \"byte_length\": 19458,\n \"sha256\": \"3d87df984af6ae5decf7ca8c3a8d5422be6c647e6a05490225b6b2037bf3701b\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"generated_descriptor_registration\",\n \"path\": \"crates/event_store/src/generated.rs\",\n \"byte_length\": 144,\n \"sha256\": \"6b0a8d6f249bd4fc3f878d37cb5e418680f0f1be2d9eec2518dedf03efc47121\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"public_surface\",\n \"path\": \"crates/event_store/src/lib.rs\",\n \"byte_length\": 3860,\n \"sha256\": \"de1c5cc1ab36e1166d23e2b76aeae1d5f0f401cf07ce243590741c0126d02132\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"migration_registry\",\n \"path\": \"crates/event_store/src/migrations.rs\",\n \"byte_length\": 73585,\n \"sha256\": \"a47477d04759ec6e71d14d3d05459864eae78fce0e3c2481a1b03147f04637f7\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"predecessor_model_public_surface\",\n \"path\": \"crates/event_store/src/model.rs\",\n \"byte_length\": 32070,\n \"sha256\": \"2cdbf6d1a4e0fa6680f45f02758513509339a7cf3a2387e824fb73b0edf749a4\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_store_reconciliation_v1_model\",\n \"path\": \"crates/event_store/src/model/reconciliation_v1.rs\",\n \"byte_length\": 10446,\n \"sha256\": \"475b7b840bffdae7e3f7a31f5b940b9dfa579eaa1cac640729aaaeadabf83ba6\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_store_reconciliation_v1_ingest_model\",\n \"path\": \"crates/event_store/src/model/ingest_reconciliation_v1.rs\",\n \"byte_length\": 1588,\n \"sha256\": \"6117e797674c35bb1ccebffbc6a8c0108bfc38c7c9f82607066dfd802b8f0734\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"addressable_transition_feed_model\",\n \"path\": \"crates/event_store/src/model/addressable_transition_feed_v1.rs\",\n \"byte_length\": 22229,\n \"sha256\": \"e651952d73ed0a29d2b137076f56f699beada205ac84ed8a96834e86e343fa78\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"current_visibility_model\",\n \"path\": \"crates/event_store/src/model/current_visibility_v1.rs\",\n \"byte_length\": 5634,\n \"sha256\": \"d64429bc7985231e923c1c3e5295d94270a817dd5dc96693f35dfdf2dc398e4f\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"food_projection_model\",\n \"path\": \"crates/event_store/src/model/food_availability_projection_v1.rs\",\n \"byte_length\": 17108,\n \"sha256\": \"67861a966d674efbc40f0b0433db2ec006048ff923f6261df4161e2ddba717b5\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"source_generation_rebuild_authority\",\n \"path\": \"crates/event_store/src/nip09/reconciliation_v1.rs\",\n \"byte_length\": 184833,\n \"sha256\": \"3ca802cb7a8444c8512b2be1390a019281704316a430fb9b75a8ad2cad358a07\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"nip09_successor_result_vector_executor\",\n \"path\": \"crates/event_store/tests/support/nip09_reconciliation_v1_result_vector_v2.rs\",\n \"byte_length\": 18205,\n \"sha256\": \"c632beee70dc777b8dd2a4f88cadb55296fee7aa5742e6e2c9f6d49f26ae2c78\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"schema_migration_and_reopen_authority\",\n \"path\": \"crates/event_store/src/schema.rs\",\n \"byte_length\": 146146,\n \"sha256\": \"93b060e80d3edd73f86208e4bf698fa9d53eaf1eeb04526c9261fb8b5726fb0d\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"public_store_and_transaction_authority\",\n \"path\": \"crates/event_store/src/store.rs\",\n \"byte_length\": 393218,\n \"sha256\": \"64565c3bc043779e1c2f26d9761901b109b4acab0f48c2021102c0e4e1f1f726\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"addressable_transition_feed_store\",\n \"path\": \"crates/event_store/src/store/addressable_transition_feed_v1.rs\",\n \"byte_length\": 40209,\n \"sha256\": \"2cebcca9602633652a79e7041d18d3a7d3feb1388a98f4f68137b9814efcedfa\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"current_visibility_store\",\n \"path\": \"crates/event_store/src/store/current_visibility_v1.rs\",\n \"byte_length\": 15737,\n \"sha256\": \"aa5d1bff7a5368cbac2b37906bb40f54bfc8aa6906bab34369569fdd105bf899\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"food_projection_store\",\n \"path\": \"crates/event_store/src/store/food_availability_projection_v1.rs\",\n \"byte_length\": 48919,\n \"sha256\": \"3a30da308ef863b806ed63d1cb65ac80f6a946a71821af84a0c595330491757e\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"predecessor_post_core_v1_extension\",\n \"path\": \"crates/event_store/src/store/post_core_extensions_v1.rs\",\n \"byte_length\": 6569,\n \"sha256\": \"f5bd8ddb45e1b2144895bd8da737fc92db051be1844ce7e8047a32b47376ce37\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"predecessor_post_core_v1_storage\",\n \"path\": \"crates/event_store/src/store/post_core_storage_v1.rs\",\n \"byte_length\": 16538,\n \"sha256\": \"fcd6546c23a6cba12b70d92728d19b6ee5d43174b83495f820348b77efa33aab\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"raw_ingest_capacity_authority\",\n \"path\": \"crates/event_store/src/store/protocol_reconciliation_v1.rs\",\n \"byte_length\": 29950,\n \"sha256\": \"c72aaff06e4f35d0b4523c6625e83345879f9e15b9fe9563dccf589e5d01e277\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"predecessor_protocol_storage\",\n \"path\": \"crates/event_store/src/store/protocol_storage_v1.rs\",\n \"byte_length\": 10835,\n \"sha256\": \"c82179b9f57968191f3e71dd008cc701787c62715ada74090eeecaa3b213076d\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"source_maintenance_runtime\",\n \"path\": \"crates/event_store/src/source_maintenance_v1.rs\",\n \"byte_length\": 51849,\n \"sha256\": \"db43df5849c811d767a87bfa7884ce499400d0b169734e18242542a4415b3a51\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"predecessor_food_result_vector_executor\",\n \"path\": \"crates/event_store/tests/food_availability_projection_v1_result_vector.rs\",\n \"byte_length\": 34046,\n \"sha256\": \"776903c6431ff07f26c6ad6b713db5628efce1a72f34d86de6e47d4404fa3a6d\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"artifact_transaction_authority\",\n \"path\": \"tools/xtask/src/contract/artifact_bundle.rs\",\n \"byte_length\": 39149,\n \"sha256\": \"3d1bba3980b80698d2a21e26caf7af6fe4d9dc4a7fbabc6e406b5d33928b457f\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"predecessor_successor_governance\",\n \"path\": \"tools/xtask/src/contract/food_availability_projection.rs\",\n \"byte_length\": 198991,\n \"sha256\": \"36fd3d8b5dda3b0855a06a47dae14cae7f4cc84aaca9902c31477c218859fccc\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"transitive_predecessor_membership_governance\",\n \"path\": \"tools/xtask/src/contract/nip09_reconciliation.rs\",\n \"byte_length\": 842433,\n \"sha256\": \"c5361d9da74ac6cbec1baa890818d5b00c935c5015c8e0cfaa9036a94fe2b69e\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"source_maintenance_governance\",\n \"path\": \"tools/xtask/src/contract/source_maintenance.rs\",\n \"byte_length\": 199270,\n \"sha256\": \"65b83a3ee1a700527d15c7403ca042cf2aaa4cd8935a2f8d2ab190c491a815bd\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"contract_command_authority\",\n \"path\": \"tools/xtask/src/contract.rs\",\n \"byte_length\": 507854,\n \"sha256\": \"92b1fd2cea9f3015285000550a75ca123ffe21f46556e8a4d81b4e3c180f0586\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"dto_root_generation_authority\",\n \"path\": \"tools/xtask/src/dto_roots.rs\",\n \"byte_length\": 35940,\n \"sha256\": \"a899a5695d7533c151d03477c4bd697a6e55c47a4980549724ac42046af694f5\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"xtask_dispatch_and_release_preflight\",\n \"path\": \"tools/xtask/src/main.rs\",\n \"byte_length\": 16426,\n \"sha256\": \"abc24896c6c740c10811a3e003ec4be8ebdb376a7b6b01445bfa7a5889f094a7\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n }\n ],\n \"public_api\": {\n \"inherited_predecessor_symbols\": [\n \"RADROOTS_ADDRESSABLE_TRANSITION_CURSOR_JSON_MAX_BYTES_V1\",\n \"RADROOTS_ADDRESSABLE_TRANSITION_D_TAG_MAX_BYTES_V1\",\n \"RADROOTS_ADDRESSABLE_TRANSITION_FEED_VERSION_V1\",\n \"RADROOTS_ADDRESSABLE_TRANSITION_PAGE_LIMIT_MAX_V1\",\n \"RADROOTS_ADDRESSABLE_TRANSITION_PAGE_RAW_JSON_MAX_BYTES_V1\",\n \"RADROOTS_ADDRESSABLE_TRANSITION_PAGE_SCAN_MAX_V1\",\n \"RADROOTS_ADDRESSABLE_TRANSITION_SCOPE_KIND_MAX_V1\",\n \"RADROOTS_FOOD_AVAILABILITY_PROJECTION_APPLY_PAGE_LIMIT_V1\",\n \"RADROOTS_FOOD_AVAILABILITY_PROJECTION_VERSION_V1\",\n \"RADROOTS_FOOD_AVAILABILITY_SEARCH_QUERY_MAX_BYTES_V1\",\n \"RADROOTS_FOOD_AVAILABILITY_SEARCH_QUERY_MAX_TERMS_V1\",\n \"RadrootsAddressableTransitionCauseV1\",\n \"RadrootsAddressableTransitionCoordinateV1\",\n \"RadrootsAddressableTransitionCursorV1\",\n \"RadrootsAddressableTransitionEventReferenceV1\",\n \"RadrootsAddressableTransitionOriginV1\",\n \"RadrootsAddressableTransitionPageV1\",\n \"RadrootsAddressableTransitionRawHeadDecisionV1\",\n \"RadrootsAddressableTransitionScopeFingerprintV1\",\n \"RadrootsAddressableTransitionScopeV1\",\n \"RadrootsAddressableTransitionV1\",\n \"RadrootsAddressableTransitionVisibilityV1\",\n \"RadrootsCurrentEventVisibilityV1\",\n \"RadrootsCurrentVisibilityDecisionV1\",\n \"RadrootsFoodAvailabilitySearchQueryV1\",\n \"RadrootsFoodAvailabilityStatusFilterV1\",\n \"RadrootsNip09SuppressionEvidenceV1\",\n \"RadrootsNip09SuppressionOutcome\",\n \"RadrootsNip09SuppressionReason\",\n \"RadrootsStoreProducedCanonicalEventV1\",\n \"RadrootsStoredFoodAvailabilityImageV1\",\n \"RadrootsStoredFoodAvailabilityV1\"\n ],\n \"added_symbols\": [\n \"RADROOTS_EVENT_STORE_RAW_EVENT_COUNT_LIMIT_V1\",\n \"RADROOTS_EVENT_STORE_RAW_EVENT_TEXT_BYTES_LIMIT_V1\",\n \"RADROOTS_EVENT_STORE_RAW_TAG_COUNT_LIMIT_V1\",\n \"RADROOTS_EVENT_STORE_RAW_TAG_TEXT_BYTES_LIMIT_V1\",\n \"RADROOTS_EVENT_STORE_RETAINED_SOURCE_GENERATION_LIMIT_V1\",\n \"RadrootsEventStoreSourceCapacityResourceV1\",\n \"RadrootsEventStoreSourceCapacityV1\"\n ],\n \"methods\": [\n \"RadrootsEventStore::source_capacity_v1\",\n \"RadrootsEventStoreSourceCapacityResourceV1::as_str\",\n \"RadrootsEventStoreSourceCapacityV1::source_generation\",\n \"RadrootsEventStoreSourceCapacityV1::raw_event_count\",\n \"RadrootsEventStoreSourceCapacityV1::raw_tag_count\",\n \"RadrootsEventStoreSourceCapacityV1::raw_event_text_bytes\",\n \"RadrootsEventStoreSourceCapacityV1::raw_tag_text_bytes\",\n \"RadrootsEventStoreSourceCapacityV1::raw_high_water_seq\",\n \"RadrootsEventStoreSourceCapacityV1::retained_generation_count\",\n \"RadrootsEventStoreSourceCapacityV1::retained_generation_limit\"\n ],\n \"error_variants\": [\n \"SourceCapacityExceeded\",\n \"SourceGenerationHistoryLimitReached\",\n \"PersistedEphemeralRawEvent\",\n \"SourceCapacityStateDrift\",\n \"SqliteMainDatabaseEncodingNotUtf8\",\n \"RollbackWouldDiscardSourceGenerationHistory\"\n ],\n \"removed_symbols\": [\n \"RadrootsEventStoreReconciliationResource\",\n \"RadrootsEventStoreError::ReconciliationCapacityExceeded\"\n ],\n \"breaking_replacements\": [\n {\n \"removed\": \"RadrootsEventStoreReconciliationResource\",\n \"replacement\": \"RadrootsEventStoreSourceCapacityResourceV1\"\n },\n {\n \"removed\": \"RadrootsEventStoreError::ReconciliationCapacityExceeded\",\n \"replacement\": \"RadrootsEventStoreError::SourceCapacityExceeded\"\n }\n ]\n },\n \"result_vector\": {\n \"canonical_path\": \"contracts/conformance/vectors/event_store/source_maintenance.v1.json\",\n \"mirror_path\": \"crates/event_store/tests/fixtures/source_maintenance.v1.json\",\n \"byte_length\": 16253,\n \"sha256\": \"997aba2604a2b9d199fb87dc9d07942ca50d91863aeadcf3eeacf16d191dd71f\",\n \"hash_algorithm\": \"sha256_bytes_v1\",\n \"executor_id\": \"radroots_event_store.source_maintenance_v1.result_vector_executor.v1\",\n \"executor_path\": \"crates/event_store/tests/source_maintenance_v1_result_vector.rs\",\n \"executor_test\": \"source_maintenance_v1_result_vector\",\n \"executor_byte_length\": 23510,\n \"executor_sha256\": \"a7487afdfe19fc5fc794811d0f0e6035203e1aabcf0a33a1d398f6b3555d38f3\",\n \"executor_hash_algorithm\": \"sha256_bytes_v1\"\n }\n}\n";
pub(crate) const SOURCE_MAINTENANCE_MANIFEST_BYTE_LENGTH: usize = 38397;
pub(crate) const SOURCE_MAINTENANCE_MANIFEST_SHA256: &str =
- "194c8175537655f006e1bc778b6588703001cb4fec382cbdbc5fb1d011ee6500";
+ "ba95f802e05eb85c5b2ffb65da747e503a7f8d58849096bc7835a30cee0c392f";
pub(crate) const SOURCE_MAINTENANCE_MANIFEST_SCHEMA_VERSION: u32 = 1;
pub(crate) const SOURCE_MAINTENANCE_CONTRACT_ID: &str =
"radroots_event_store.source_maintenance_v1";
diff --git a/crates/nostr/tests/package_boundary.rs b/crates/nostr/tests/package_boundary.rs
@@ -246,18 +246,21 @@ fn live_client_and_http_ownership_belongs_to_transport_nostr() {
);
}
- for required in ["nostr-sdk", "reqwest", "nip11 = [\"client\""] {
+ for required in ["nostr-sdk", "radroots_transport", "radroots_event_codec"] {
assert!(
TRANSPORT_MANIFEST.contains(required),
"transport manifest is missing live-client ownership marker `{required}`"
);
}
for required in [
+ "mod auth;",
"mod client;",
- "mod relays;",
- "mod nip11;",
- "pub use client::{",
- "pub use nip11::fetch_nip11;",
+ "mod relay;",
+ "mod sink;",
+ "mod source;",
+ "mod status;",
+ "pub use client::{Config, NostrTransport};",
+ "pub use relay::{RelayUrl, RelayUrlPolicy};",
] {
assert!(
TRANSPORT_ROOT.contains(required),
@@ -275,15 +278,16 @@ fn live_client_and_http_ownership_belongs_to_transport_nostr() {
.exists()
);
assert!(
- manifest_dir
+ !manifest_dir
.join("../transport_nostr/src/relays.rs")
.exists()
);
assert!(
- manifest_dir
+ !manifest_dir
.join("../transport_nostr/src/nip11.rs")
.exists()
);
+ assert!(!TRANSPORT_MANIFEST.contains("reqwest"));
let forbidden_source = [
"nostr_sdk",
diff --git a/crates/transport/tests/source_boundary.rs b/crates/transport/tests/source_boundary.rs
@@ -567,14 +567,19 @@ fn workspace_consumers_use_split_transport_spis_with_one_runtime_shim() {
"Reticulum preview must not implement the predecessor monolithic SPI"
);
- let nostr_source = read_source(crates_root.join("transport_nostr/src/publish.rs").as_path());
+ let nostr_sink = read_source(crates_root.join("transport_nostr/src/sink.rs").as_path());
+ let nostr_source = read_source(crates_root.join("transport_nostr/src/source.rs").as_path());
assert!(
- nostr_source.contains("impl<A> EventSink for RadrootsNostrTransport<A>"),
- "Nostr adapter must implement the final sink-only SPI"
+ nostr_sink.contains("impl EventSink for NostrTransport"),
+ "Nostr adapter must implement the final sink SPI"
);
assert!(
- !nostr_source
- .contains("impl<A> RadrootsRuntimeTransportShim for RadrootsNostrTransport<A>"),
+ nostr_source.contains("impl EventSource for NostrTransport"),
+ "Nostr adapter must implement the final source SPI"
+ );
+ assert!(
+ !nostr_sink.contains("RadrootsRuntimeTransportShim")
+ && !nostr_source.contains("RadrootsRuntimeTransportShim"),
"Nostr adapter must not implement the predecessor monolithic SPI"
);
}
@@ -735,12 +740,15 @@ fn transport_target_identity_sources_reject_silent_dedupe() {
);
let relay_source = read_source(crates_root.join("transport_nostr/src/relay.rs").as_path());
- for required in [
- "Target::new(TransportId::NOSTR, original)",
- "RadrootsRelayTransportError::DuplicateRelayUrl",
- ] {
+ let client_source = read_source(crates_root.join("transport_nostr/src/client.rs").as_path());
+ for required in ["Target::nostr_relay(original)", "Error::DuplicateRelayUrl"] {
+ let source = if required.contains("Duplicate") {
+ client_source.as_str()
+ } else {
+ relay_source.as_str()
+ };
assert!(
- relay_source.contains(required),
+ source.contains(required),
"Nostr relay target source must retain canonical identity witness `{required}`"
);
}
@@ -813,39 +821,19 @@ fn required_target_semantics_stay_fingerprint_exact() {
}
let nostr_publish_source =
- read_source(crates_root.join("transport_nostr/src/publish.rs").as_path());
+ read_source(crates_root.join("transport_nostr/src/sink.rs").as_path());
for required in [
- "RadrootsTransportSatisfactionPolicy::RequiredTargets { class, targets } =>",
- "let mut satisfied_required_targets = BTreeSet::new();",
- "targets.contains(target.fingerprint())",
- "counts_as_satisfied(*class)",
- "targets\n .iter()\n .all(|target| satisfied_required_targets.contains(target))",
+ "DeliveryReceipt::for_request(&request, receipts)",
+ "DeliveryTargetReceipt::attempted(target, outcome)",
+ "DeliveryTargetReceipt::skipped(",
] {
assert!(
nostr_publish_source.contains(required),
- "direct Nostr publish must retain exact required-target witness `{required}`"
- );
- }
-
- let nostr_outbox_source =
- read_source(crates_root.join("transport_nostr/src/outbox.rs").as_path());
- let publishable_relays_source = source_between(
- nostr_outbox_source.as_str(),
- "let required_targets = match &plan.satisfaction_policy",
- "Ok(PublishableRelays {",
- );
- for required in [
- "RadrootsTransportSatisfactionPolicy::RequiredTargets { targets, .. } =>",
- ".is_none_or(|required| required.contains(&target.endpoint_fingerprint))",
- ".is_some_and(|required| required.contains(&target.endpoint_fingerprint))",
- "required_targets.is_none() || required_for_satisfaction",
- "required_targets.is_some()",
- ] {
- assert!(
- publishable_relays_source.contains(required),
- "direct Nostr outbox publish must retain exact required-target witness `{required}`"
+ "Nostr sink must delegate exact target satisfaction to the generic receipt contract `{required}`"
);
}
+ assert!(!crates_root.join("transport_nostr/src/outbox.rs").exists());
+ assert!(!crates_root.join("transport_nostr/src/publish.rs").exists());
}
#[test]
diff --git a/crates/transport_nostr/README.md b/crates/transport_nostr/README.md
@@ -1,43 +1,13 @@
# radroots_transport_nostr
-Deterministic Nostr relay transport substrate for exact signed-event publish,
-fetch ingest, and outbox delivery target coordination.
+`radroots_transport_nostr` is the concrete, signer-free Nostr implementation
+of the generic `radroots_transport` event source and sink interfaces.
-Every fetch path verifies the NIP-01 id and signature before filter matching,
-unique-event budgeting, or returning an event. Repeated event ids preserve
-per-relay observation evidence without consuming the unique-event limit. The
-unique-event limit is bounded at 1,000 so final stored-event visibility can be
-evaluated in one coherent event-store snapshot. A fetch scans at most 64,000
-raw events and 64 MiB of aggregate raw JSON, and rejects any individual raw
-event over 256 KiB before Radroots parses adapter raw JSON. Count and byte
-budgets are charged globally, in adapter order, before Radroots parsing,
-filtering, deduplication, or accepted-event limiting, so malformed and otherwise
-rejected events cannot bypass them. The official SDK adapter enforces the same
-retained-prefix budgets after SDK frame/event decoding and before retaining its
-serialized JSON; the connector's upstream frame parser remains responsible for
-its own first-pass network limits. `Truncated` is distinct from relay `EOSE`,
-including for later target relays that were not queried after a global count or
-byte budget was reached.
+The crate validates relay configuration and network policy, performs bounded
+fetch and delivery attempts, exposes explicit host-mediated NIP-42
+authentication, and normalizes relay outcomes and passive status. It does not
+own event ingestion, persistence, outbox claiming, projection refresh, retry
+scheduling, SDK profiles, or a process runtime. Those policies belong to
+`radroots_sync` and host applications.
-Fetch-ingest receipts report verification, contract admission, immutable
-valid-stream eligibility, and current visibility as independent exhaustive
-enums. Verification failure is not a contract-invalid result, and unsupported
-admission does not hide whether the stored event is current, suppressed, or not
-admitted. Persisted events obtain visibility from the event store's central
-authority after the complete accepted fetch batch has been ingested, so event
-receipts and aggregate visibility counts describe final post-batch state rather
-than transient per-item state. Repeated receipt IDs are deduplicated before the
-single snapshot lookup and then mapped back to every receipt. Ephemeral events
-use the explicit `not_persisted` visibility result.
-`admission_code` carries the stable classifier diagnostic when classification
-produces one. Inserted, duplicate, and not-persisted persistence outcomes retain
-separate flags and aggregate counts. Local event-store failures abort the
-operation and remain typed transport errors, so callers can retry without
-confusing storage failure with bad relay input.
-
-`RadrootsRelayUrlPolicy::Public` is for trusted relay configuration. It rejects
-non-canonical and known non-global literal destinations, but hostname checks do
-not pin DNS resolution in the SDK connector and are not an SSRF boundary for
-attacker-controlled relay hostnames. Validate resolved addresses at the network
-boundary or use a connector that pins approved resolutions before accepting
-untrusted relay configuration.
+Publication remains disabled during the `0.1.0-alpha` refactor.
diff --git a/crates/transport_nostr/src/fetch.rs b/crates/transport_nostr/src/fetch.rs
@@ -1,1584 +0,0 @@
-#![forbid(unsafe_code)]
-
-use crate::RadrootsNostrClient;
-use crate::error::ensure_nonnegative_timestamp;
-use crate::{RadrootsRelayOutcome, RadrootsRelayTargetSet, RadrootsRelayTransportError};
-use core::time::Duration;
-use futures::{StreamExt, future::BoxFuture};
-use nostr::{JsonUtil, filter::MatchEventOptions};
-use radroots_event::wire::v1::DEFAULT_RAW_JSON_MAX_BYTES;
-use radroots_event_store::{
- RADROOTS_EVENT_STORE_QUERY_LIMIT_MAX, RadrootsEventAdmissionStatus, RadrootsEventIngest,
- RadrootsEventPersistence, RadrootsEventStore, RadrootsEventVisibility,
- RadrootsTransportObservation, RadrootsTransportObservationType,
-};
-use radroots_nostr::event::Event as RadrootsNostrEvent;
-use radroots_nostr::filter::Filter as RadrootsNostrFilter;
-use radroots_transport::{Target, TransportId};
-use serde::{Deserialize, Serialize};
-use std::collections::{BTreeMap, BTreeSet};
-use std::sync::{Arc, Mutex, PoisonError};
-
-const DEFAULT_RELAY_FETCH_TIMEOUT_MS: u64 = 10_000;
-const DEFAULT_RELAY_FETCH_RAW_SCAN_MULTIPLIER: usize = 64;
-pub const RADROOTS_RELAY_FETCH_EVENT_LIMIT_MAX: usize =
- RADROOTS_EVENT_STORE_QUERY_LIMIT_MAX as usize;
-pub const RADROOTS_RELAY_FETCH_RAW_EVENT_LIMIT_MAX: usize =
- RADROOTS_RELAY_FETCH_EVENT_LIMIT_MAX * DEFAULT_RELAY_FETCH_RAW_SCAN_MULTIPLIER;
-pub const RADROOTS_RELAY_FETCH_RAW_JSON_BYTE_LIMIT_MAX: usize = 64 * 1024 * 1024;
-
-#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
-pub enum RadrootsRelayFetchMode {
- Fetch,
- Subscription,
-}
-
-#[derive(Clone, Debug, PartialEq, Eq)]
-pub struct RadrootsRelayFetchFilters {
- filters: Vec<RadrootsNostrFilter>,
-}
-
-impl RadrootsRelayFetchFilters {
- pub fn new<I>(filters: I) -> Result<Self, RadrootsRelayTransportError>
- where
- I: IntoIterator<Item = RadrootsNostrFilter>,
- {
- let filters = filters.into_iter().collect::<Vec<_>>();
- if filters.is_empty() {
- return Err(RadrootsRelayTransportError::EmptyFetchFilters);
- }
- Ok(Self { filters })
- }
-
- pub fn as_slice(&self) -> &[RadrootsNostrFilter] {
- &self.filters
- }
-}
-
-impl AsRef<[RadrootsNostrFilter]> for RadrootsRelayFetchFilters {
- fn as_ref(&self) -> &[RadrootsNostrFilter] {
- self.as_slice()
- }
-}
-
-#[derive(Clone, Debug, PartialEq, Eq)]
-pub struct RadrootsRelayFetchRequest {
- mode: RadrootsRelayFetchMode,
- observed_at_ms: i64,
- max_events: usize,
- max_raw_events: usize,
- max_raw_json_bytes: usize,
- relay_targets: RadrootsRelayTargetSet,
- filters: RadrootsRelayFetchFilters,
- timeout_ms: u64,
-}
-
-impl RadrootsRelayFetchRequest {
- pub fn fetch<I>(
- observed_at_ms: i64,
- max_events: usize,
- relay_targets: RadrootsRelayTargetSet,
- filters: I,
- ) -> Result<Self, RadrootsRelayTransportError>
- where
- I: IntoIterator<Item = RadrootsNostrFilter>,
- {
- Self::new(
- RadrootsRelayFetchMode::Fetch,
- observed_at_ms,
- max_events,
- relay_targets,
- filters,
- )
- }
-
- pub fn subscription<I>(
- observed_at_ms: i64,
- max_events: usize,
- relay_targets: RadrootsRelayTargetSet,
- filters: I,
- ) -> Result<Self, RadrootsRelayTransportError>
- where
- I: IntoIterator<Item = RadrootsNostrFilter>,
- {
- Self::new(
- RadrootsRelayFetchMode::Subscription,
- observed_at_ms,
- max_events,
- relay_targets,
- filters,
- )
- }
-
- fn new<I>(
- mode: RadrootsRelayFetchMode,
- observed_at_ms: i64,
- max_events: usize,
- relay_targets: RadrootsRelayTargetSet,
- filters: I,
- ) -> Result<Self, RadrootsRelayTransportError>
- where
- I: IntoIterator<Item = RadrootsNostrFilter>,
- {
- ensure_nonnegative_timestamp("observed_at_ms", observed_at_ms)?;
- ensure_event_limit("max_events", max_events)?;
- Ok(Self {
- mode,
- observed_at_ms,
- max_events,
- max_raw_events: default_raw_event_scan_limit(max_events),
- max_raw_json_bytes: RADROOTS_RELAY_FETCH_RAW_JSON_BYTE_LIMIT_MAX,
- relay_targets,
- filters: RadrootsRelayFetchFilters::new(filters)?,
- timeout_ms: DEFAULT_RELAY_FETCH_TIMEOUT_MS,
- })
- }
-
- pub fn with_timeout_ms(mut self, timeout_ms: u64) -> Result<Self, RadrootsRelayTransportError> {
- ensure_positive_timeout("timeout_ms", timeout_ms)?;
- self.timeout_ms = timeout_ms;
- Ok(self)
- }
-
- pub fn with_raw_event_scan_limit(
- mut self,
- max_raw_events: usize,
- ) -> Result<Self, RadrootsRelayTransportError> {
- ensure_bounded_fetch_limit(
- "max_raw_events",
- max_raw_events,
- RADROOTS_RELAY_FETCH_RAW_EVENT_LIMIT_MAX,
- )?;
- self.max_raw_events = max_raw_events;
- Ok(self)
- }
-
- pub fn with_raw_json_byte_limit(
- mut self,
- max_raw_json_bytes: usize,
- ) -> Result<Self, RadrootsRelayTransportError> {
- ensure_bounded_fetch_limit(
- "max_raw_json_bytes",
- max_raw_json_bytes,
- RADROOTS_RELAY_FETCH_RAW_JSON_BYTE_LIMIT_MAX,
- )?;
- self.max_raw_json_bytes = max_raw_json_bytes;
- Ok(self)
- }
-
- pub fn mode(&self) -> RadrootsRelayFetchMode {
- self.mode
- }
-
- pub fn observed_at_ms(&self) -> i64 {
- self.observed_at_ms
- }
-
- pub fn max_events(&self) -> usize {
- self.max_events
- }
-
- pub fn max_raw_events(&self) -> usize {
- self.max_raw_events
- }
-
- pub fn max_raw_json_bytes(&self) -> usize {
- self.max_raw_json_bytes
- }
-
- pub fn relay_targets(&self) -> &RadrootsRelayTargetSet {
- &self.relay_targets
- }
-
- pub fn filters(&self) -> &[RadrootsNostrFilter] {
- self.filters.as_slice()
- }
-
- pub fn timeout_ms(&self) -> u64 {
- self.timeout_ms
- }
-}
-
-fn default_raw_event_scan_limit(max_events: usize) -> usize {
- max_events
- .saturating_mul(DEFAULT_RELAY_FETCH_RAW_SCAN_MULTIPLIER)
- .max(max_events)
- .min(RADROOTS_RELAY_FETCH_RAW_EVENT_LIMIT_MAX)
-}
-
-fn ensure_positive_limit(
- field: &'static str,
- value: usize,
-) -> Result<(), RadrootsRelayTransportError> {
- if value == 0 {
- return Err(RadrootsRelayTransportError::InvalidFetchLimit { field });
- }
- Ok(())
-}
-
-fn ensure_event_limit(
- field: &'static str,
- value: usize,
-) -> Result<(), RadrootsRelayTransportError> {
- ensure_bounded_fetch_limit(field, value, RADROOTS_RELAY_FETCH_EVENT_LIMIT_MAX)
-}
-
-fn ensure_bounded_fetch_limit(
- field: &'static str,
- value: usize,
- max: usize,
-) -> Result<(), RadrootsRelayTransportError> {
- ensure_positive_limit(field, value)?;
- if value > max {
- return Err(RadrootsRelayTransportError::FetchLimitTooLarge {
- field,
- max,
- actual: value,
- });
- }
- Ok(())
-}
-
-fn ensure_positive_timeout(
- field: &'static str,
- value: u64,
-) -> Result<(), RadrootsRelayTransportError> {
- if value == 0 {
- return Err(RadrootsRelayTransportError::InvalidFetchLimit { field });
- }
- Ok(())
-}
-
-#[derive(Clone, Debug, PartialEq, Eq)]
-pub enum RadrootsRelayFetchItem {
- Event { relay_url: String, raw_json: String },
- Eose { relay_url: String },
- Truncated { relay_url: String, message: String },
- Closed { relay_url: String, message: String },
- Notice { relay_url: String, message: String },
-}
-
-#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
-pub enum RadrootsRelayFetchOutcomeKind {
- Eose,
- Truncated,
- Closed,
- Notice,
-}
-
-#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
-pub struct RadrootsRelayFetchRelayOutcome {
- pub relay_url: String,
- pub kind: RadrootsRelayFetchOutcomeKind,
- pub relay_outcome: Option<RadrootsRelayOutcome>,
- pub message: Option<String>,
-}
-
-#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
-#[serde(rename_all = "snake_case")]
-pub enum RadrootsRelayFetchEventVerification {
- NotEvaluated,
- Verified,
- Failed,
-}
-
-#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
-#[serde(rename_all = "snake_case")]
-pub enum RadrootsRelayFetchEventAdmission {
- NotEvaluated,
- Admitted,
- Unsupported,
- Invalid,
-}
-
-#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
-#[serde(rename_all = "snake_case")]
-pub enum RadrootsRelayFetchEventValidStream {
- NotEvaluated,
- Eligible,
- Ineligible,
-}
-
-#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
-#[serde(rename_all = "snake_case")]
-pub enum RadrootsRelayFetchEventVisibility {
- NotEvaluated,
- NotPersisted,
- Visible,
- NotAdmitted,
- NotCurrent,
- Suppressed,
-}
-
-#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
-pub struct RadrootsRelayFetchEventReceipt {
- pub relay_url: String,
- pub event_id: Option<String>,
- pub inserted: bool,
- pub duplicate: bool,
- pub not_persisted: bool,
- pub malformed: bool,
- pub out_of_filter: bool,
- pub skipped_over_limit: bool,
- pub verification: RadrootsRelayFetchEventVerification,
- pub admission: RadrootsRelayFetchEventAdmission,
- pub admission_code: Option<String>,
- pub valid_stream: RadrootsRelayFetchEventValidStream,
- pub visibility: RadrootsRelayFetchEventVisibility,
- pub message: Option<String>,
-}
-
-#[derive(Clone, Debug)]
-pub struct RadrootsRelayFetchedEvent {
- pub relay_url: String,
- pub event: RadrootsNostrEvent,
- pub raw_json: String,
- pub observed_at_ms: i64,
-}
-
-#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
-pub struct RadrootsRelayFetchFailure {
- pub relay_url: String,
- pub reason: String,
-}
-
-#[derive(Clone, Debug)]
-pub struct RadrootsRelayFetchedEventsReceipt {
- pub target_relays: Vec<String>,
- pub connected_relays: Vec<String>,
- pub failed_relays: Vec<RadrootsRelayFetchFailure>,
- pub events: Vec<RadrootsRelayFetchedEvent>,
- pub event_receipts: Vec<RadrootsRelayFetchEventReceipt>,
- pub duplicate_count: usize,
- pub verification_failed_count: usize,
- pub malformed_count: usize,
- pub out_of_filter_count: usize,
- pub skipped_over_limit_count: usize,
- pub eose_count: usize,
- pub truncated_count: usize,
- pub closed_count: usize,
- pub notice_count: usize,
- pub relay_outcomes: Vec<RadrootsRelayFetchRelayOutcome>,
-}
-
-#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
-pub struct RadrootsRelayFetchReceipt {
- pub inserted_count: usize,
- pub duplicate_count: usize,
- pub not_persisted_count: usize,
- pub malformed_count: usize,
- pub out_of_filter_count: usize,
- pub skipped_over_limit_count: usize,
- pub verification_failed_count: usize,
- pub admission_unsupported_count: usize,
- pub admission_invalid_count: usize,
- pub valid_stream_eligible_count: usize,
- pub visible_count: usize,
- pub not_admitted_count: usize,
- pub not_current_count: usize,
- pub suppressed_count: usize,
- pub eose_count: usize,
- pub truncated_count: usize,
- pub closed_count: usize,
- pub notice_count: usize,
- pub events: Vec<RadrootsRelayFetchEventReceipt>,
- pub relay_outcomes: Vec<RadrootsRelayFetchRelayOutcome>,
-}
-
-pub trait RadrootsRelayFetchAdapter: Send + Sync {
- fn fetch<'a>(
- &'a self,
- request: RadrootsRelayFetchRequest,
- ) -> BoxFuture<'a, Result<Vec<RadrootsRelayFetchItem>, RadrootsRelayTransportError>>;
-}
-
-pub async fn fetch_relay_events<A>(
- adapter: &A,
- request: RadrootsRelayFetchRequest,
-) -> Result<RadrootsRelayFetchedEventsReceipt, RadrootsRelayTransportError>
-where
- A: RadrootsRelayFetchAdapter,
-{
- let target_relays = request.relay_targets.relay_strings();
- let observed_at_ms = request.observed_at_ms;
- let max_events = request.max_events;
- let max_raw_events = request.max_raw_events;
- let max_raw_json_bytes = request.max_raw_json_bytes;
- let filters = request.filters.as_slice().to_vec();
- let items = adapter.fetch(request).await?;
- Ok(process_relay_fetch_items(
- target_relays,
- filters,
- observed_at_ms,
- max_events,
- max_raw_events,
- max_raw_json_bytes,
- items,
- )?
- .into_fetched_events_receipt())
-}
-
-#[cfg(feature = "runtime-tokio")]
-#[cfg_attr(coverage_nightly, coverage(off))]
-pub fn fetch_relay_events_blocking<A>(
- adapter: &A,
- request: RadrootsRelayFetchRequest,
-) -> Result<RadrootsRelayFetchedEventsReceipt, RadrootsRelayTransportError>
-where
- A: RadrootsRelayFetchAdapter,
-{
- let runtime = tokio::runtime::Builder::new_current_thread()
- .enable_all()
- .build()
- .map_err(|error| RadrootsRelayTransportError::Transport(error.to_string()))?;
- runtime.block_on(fetch_relay_events(adapter, request))
-}
-
-pub async fn fetch_and_ingest_relay_events<A>(
- adapter: &A,
- event_store: &RadrootsEventStore,
- request: RadrootsRelayFetchRequest,
-) -> Result<RadrootsRelayFetchReceipt, RadrootsRelayTransportError>
-where
- A: RadrootsRelayFetchAdapter,
-{
- let mode = request.mode;
- let target_relays = request.relay_targets.relay_strings();
- let observed_at_ms = request.observed_at_ms;
- let max_events = request.max_events;
- let max_raw_events = request.max_raw_events;
- let max_raw_json_bytes = request.max_raw_json_bytes;
- let filters = request.filters.as_slice().to_vec();
- let items = adapter.fetch(request).await?;
- let processed = process_relay_fetch_items(
- target_relays,
- filters,
- observed_at_ms,
- max_events,
- max_raw_events,
- max_raw_json_bytes,
- items,
- )?;
- let mut receipt = RadrootsRelayFetchReceipt::from_processed_counts(&processed);
- for item in processed.items {
- match item {
- RadrootsRelayProcessedFetchItem::Receipt(event_receipt) => {
- receipt.events.push(event_receipt);
- }
- RadrootsRelayProcessedFetchItem::Accepted(RadrootsRelayFetchedEvent {
- relay_url,
- event: raw_event,
- raw_json,
- observed_at_ms,
- })
- | RadrootsRelayProcessedFetchItem::Duplicate(RadrootsRelayFetchedEvent {
- relay_url,
- event: raw_event,
- raw_json,
- observed_at_ms,
- }) => {
- let observation_type = match mode {
- RadrootsRelayFetchMode::Fetch => RadrootsTransportObservationType::Fetch,
- RadrootsRelayFetchMode::Subscription => {
- RadrootsTransportObservationType::Subscription
- }
- };
- let observation = RadrootsTransportObservation::new(
- TransportId::NOSTR,
- relay_url.clone(),
- observation_type,
- observed_at_ms,
- )?;
- let ingest = match RadrootsEventIngest::from_raw_json(raw_json, observed_at_ms) {
- Ok(ingest) => ingest.with_observation(observation),
- Err(error) => {
- receipt.verification_failed_count += 1;
- receipt.events.push(RadrootsRelayFetchEventReceipt {
- relay_url,
- event_id: Some(raw_event.id.to_hex()),
- inserted: false,
- duplicate: false,
- not_persisted: false,
- malformed: false,
- out_of_filter: false,
- skipped_over_limit: false,
- verification: RadrootsRelayFetchEventVerification::Failed,
- admission: RadrootsRelayFetchEventAdmission::NotEvaluated,
- admission_code: None,
- valid_stream: RadrootsRelayFetchEventValidStream::NotEvaluated,
- visibility: RadrootsRelayFetchEventVisibility::NotEvaluated,
- message: Some(error.to_string()),
- });
- continue;
- }
- };
- let store_receipt = event_store.ingest_event(ingest).await?;
- let admission = relay_fetch_admission(store_receipt.admission_status);
- let valid_stream = if store_receipt.valid_stream_eligible {
- RadrootsRelayFetchEventValidStream::Eligible
- } else {
- RadrootsRelayFetchEventValidStream::Ineligible
- };
- let (inserted, duplicate, not_persisted) = match store_receipt.persistence {
- RadrootsEventPersistence::Inserted { .. } => {
- receipt.inserted_count += 1;
- (true, false, false)
- }
- RadrootsEventPersistence::Duplicate { .. } => {
- receipt.duplicate_count += 1;
- (false, true, false)
- }
- RadrootsEventPersistence::NotPersisted => {
- receipt.not_persisted_count += 1;
- (false, false, true)
- }
- };
- let visibility = if not_persisted {
- RadrootsRelayFetchEventVisibility::NotPersisted
- } else {
- RadrootsRelayFetchEventVisibility::NotEvaluated
- };
- let event_receipt = RadrootsRelayFetchEventReceipt {
- relay_url,
- event_id: Some(store_receipt.event_id),
- inserted,
- duplicate,
- not_persisted,
- malformed: false,
- out_of_filter: false,
- skipped_over_limit: false,
- verification: RadrootsRelayFetchEventVerification::Verified,
- admission,
- admission_code: store_receipt.admission_code,
- valid_stream,
- visibility,
- message: None,
- };
- receipt.events.push(event_receipt);
- }
- }
- }
- receipt.refresh_final_semantic_outcomes(event_store).await?;
- Ok(receipt)
-}
-
-fn relay_fetch_admission(
- admission: RadrootsEventAdmissionStatus,
-) -> RadrootsRelayFetchEventAdmission {
- match admission {
- RadrootsEventAdmissionStatus::Admitted => RadrootsRelayFetchEventAdmission::Admitted,
- RadrootsEventAdmissionStatus::Unsupported => RadrootsRelayFetchEventAdmission::Unsupported,
- RadrootsEventAdmissionStatus::Invalid => RadrootsRelayFetchEventAdmission::Invalid,
- }
-}
-
-fn relay_fetch_visibility(
- event_id: &str,
- visibility: RadrootsEventVisibility,
-) -> Result<RadrootsRelayFetchEventVisibility, RadrootsRelayTransportError> {
- match visibility {
- RadrootsEventVisibility::Visible => Ok(RadrootsRelayFetchEventVisibility::Visible),
- RadrootsEventVisibility::NotAdmitted => Ok(RadrootsRelayFetchEventVisibility::NotAdmitted),
- RadrootsEventVisibility::NotCurrent { .. } => {
- Ok(RadrootsRelayFetchEventVisibility::NotCurrent)
- }
- RadrootsEventVisibility::Suppressed { .. } => {
- Ok(RadrootsRelayFetchEventVisibility::Suppressed)
- }
- _ => Err(
- RadrootsRelayTransportError::UnsupportedStoredEventVisibility {
- event_id: event_id.to_owned(),
- },
- ),
- }
-}
-
-fn required_persisted_fetch_receipt_event_id(
- event_id: Option<&str>,
-) -> Result<&str, RadrootsRelayTransportError> {
- event_id.ok_or(RadrootsRelayTransportError::MissingPersistedFetchReceiptEventId)
-}
-
-#[derive(Clone, Debug)]
-enum RadrootsRelayProcessedFetchItem {
- Accepted(RadrootsRelayFetchedEvent),
- Duplicate(RadrootsRelayFetchedEvent),
- Receipt(RadrootsRelayFetchEventReceipt),
-}
-
-#[derive(Clone, Debug)]
-struct RadrootsRelayProcessedFetch {
- target_relays: Vec<String>,
- items: Vec<RadrootsRelayProcessedFetchItem>,
- duplicate_count: usize,
- verification_failed_count: usize,
- malformed_count: usize,
- out_of_filter_count: usize,
- skipped_over_limit_count: usize,
- eose_count: usize,
- truncated_count: usize,
- closed_count: usize,
- notice_count: usize,
- relay_outcomes: Vec<RadrootsRelayFetchRelayOutcome>,
-}
-
-impl RadrootsRelayProcessedFetch {
- fn into_fetched_events_receipt(self) -> RadrootsRelayFetchedEventsReceipt {
- let mut events = Vec::new();
- let mut event_receipts = Vec::new();
- for item in self.items {
- match item {
- RadrootsRelayProcessedFetchItem::Accepted(event) => {
- event_receipts.push(accepted_fetch_event_receipt(&event));
- events.push(event);
- }
- RadrootsRelayProcessedFetchItem::Duplicate(event) => {
- event_receipts.push(duplicate_fetch_event_receipt(&event));
- }
- RadrootsRelayProcessedFetchItem::Receipt(receipt) => event_receipts.push(receipt),
- }
- }
- let connected_relays = self
- .relay_outcomes
- .iter()
- .filter(|outcome| outcome.kind == RadrootsRelayFetchOutcomeKind::Eose)
- .map(|outcome| outcome.relay_url.clone())
- .collect();
- let failed_relays = self
- .relay_outcomes
- .iter()
- .filter(|outcome| outcome.kind == RadrootsRelayFetchOutcomeKind::Closed)
- .map(|outcome| RadrootsRelayFetchFailure {
- relay_url: outcome.relay_url.clone(),
- reason: outcome.message.clone().unwrap_or_default(),
- })
- .collect();
- RadrootsRelayFetchedEventsReceipt {
- target_relays: self.target_relays,
- connected_relays,
- failed_relays,
- events,
- event_receipts,
- duplicate_count: self.duplicate_count,
- verification_failed_count: self.verification_failed_count,
- malformed_count: self.malformed_count,
- out_of_filter_count: self.out_of_filter_count,
- skipped_over_limit_count: self.skipped_over_limit_count,
- eose_count: self.eose_count,
- truncated_count: self.truncated_count,
- closed_count: self.closed_count,
- notice_count: self.notice_count,
- relay_outcomes: self.relay_outcomes,
- }
- }
-}
-
-impl RadrootsRelayFetchReceipt {
- fn from_processed_counts(processed: &RadrootsRelayProcessedFetch) -> Self {
- Self {
- inserted_count: 0,
- duplicate_count: 0,
- not_persisted_count: 0,
- malformed_count: processed.malformed_count,
- out_of_filter_count: processed.out_of_filter_count,
- skipped_over_limit_count: processed.skipped_over_limit_count,
- verification_failed_count: processed.verification_failed_count,
- admission_unsupported_count: 0,
- admission_invalid_count: 0,
- valid_stream_eligible_count: 0,
- visible_count: 0,
- not_admitted_count: 0,
- not_current_count: 0,
- suppressed_count: 0,
- eose_count: processed.eose_count,
- truncated_count: processed.truncated_count,
- closed_count: processed.closed_count,
- notice_count: processed.notice_count,
- events: Vec::new(),
- relay_outcomes: processed.relay_outcomes.clone(),
- }
- }
-
- async fn refresh_final_semantic_outcomes(
- &mut self,
- event_store: &RadrootsEventStore,
- ) -> Result<(), RadrootsRelayTransportError> {
- let mut event_ids = Vec::new();
- let mut seen_event_ids = BTreeSet::new();
- for event in &self.events {
- if event.not_persisted || (!event.inserted && !event.duplicate) {
- continue;
- }
- let event_id = required_persisted_fetch_receipt_event_id(event.event_id.as_deref())?;
- if seen_event_ids.insert(event_id) {
- event_ids.push(event_id.to_owned());
- }
- }
- let visibilities = event_store.event_visibilities(event_ids.iter()).await?;
- let visibilities_by_event_id = event_ids
- .into_iter()
- .zip(visibilities)
- .collect::<BTreeMap<_, _>>();
-
- for event in &mut self.events {
- if event.not_persisted || (!event.inserted && !event.duplicate) {
- continue;
- }
- let event_id = required_persisted_fetch_receipt_event_id(event.event_id.as_deref())?;
- let visibility = visibilities_by_event_id
- .get(event_id)
- .cloned()
- .flatten()
- .ok_or_else(
- || RadrootsRelayTransportError::MissingStoredEventVisibility {
- event_id: event_id.to_owned(),
- },
- )?;
- event.visibility = relay_fetch_visibility(event_id, visibility)?;
- }
-
- self.admission_unsupported_count = 0;
- self.admission_invalid_count = 0;
- self.valid_stream_eligible_count = 0;
- self.visible_count = 0;
- self.not_admitted_count = 0;
- self.not_current_count = 0;
- self.suppressed_count = 0;
- for event in &self.events {
- match event.admission {
- RadrootsRelayFetchEventAdmission::Unsupported => {
- self.admission_unsupported_count += 1;
- }
- RadrootsRelayFetchEventAdmission::Invalid => self.admission_invalid_count += 1,
- RadrootsRelayFetchEventAdmission::NotEvaluated
- | RadrootsRelayFetchEventAdmission::Admitted => {}
- }
- if event.valid_stream == RadrootsRelayFetchEventValidStream::Eligible {
- self.valid_stream_eligible_count += 1;
- }
- match event.visibility {
- RadrootsRelayFetchEventVisibility::Visible => self.visible_count += 1,
- RadrootsRelayFetchEventVisibility::NotAdmitted => {
- self.not_admitted_count += 1;
- }
- RadrootsRelayFetchEventVisibility::NotCurrent => self.not_current_count += 1,
- RadrootsRelayFetchEventVisibility::Suppressed => self.suppressed_count += 1,
- RadrootsRelayFetchEventVisibility::NotEvaluated
- | RadrootsRelayFetchEventVisibility::NotPersisted => {}
- }
- }
- Ok(())
- }
-}
-
-#[derive(Clone, Copy, Debug, PartialEq, Eq)]
-enum RadrootsRelayFetchRawBudgetExhaustion {
- RawEvents,
- RawJsonBytes,
-}
-
-impl RadrootsRelayFetchRawBudgetExhaustion {
- const fn current_relay_message(self) -> &'static str {
- match self {
- Self::RawEvents => "raw event scan limit reached before relay EOSE",
- Self::RawJsonBytes => "aggregate raw JSON byte limit reached before relay EOSE",
- }
- }
-
- const fn unqueried_relay_message(self) -> &'static str {
- match self {
- Self::RawEvents => {
- "relay was not queried because the global raw event scan limit was reached"
- }
- Self::RawJsonBytes => {
- "relay was not queried because the global aggregate raw JSON byte limit was reached"
- }
- }
- }
-}
-
-#[derive(Clone, Copy, Debug, PartialEq, Eq)]
-struct RadrootsRelayFetchRawBudget {
- remaining_events: usize,
- remaining_json_bytes: usize,
- exhausted: Option<RadrootsRelayFetchRawBudgetExhaustion>,
-}
-
-impl RadrootsRelayFetchRawBudget {
- const fn new(max_raw_events: usize, max_raw_json_bytes: usize) -> Self {
- Self {
- remaining_events: max_raw_events,
- remaining_json_bytes: max_raw_json_bytes,
- exhausted: None,
- }
- }
-
- fn charge(
- &mut self,
- raw_json_bytes: usize,
- ) -> Result<(), RadrootsRelayFetchRawBudgetExhaustion> {
- if let Some(reason) = self.exhausted {
- return Err(reason);
- }
- let Some(remaining_events) = self.remaining_events.checked_sub(1) else {
- self.exhausted = Some(RadrootsRelayFetchRawBudgetExhaustion::RawEvents);
- return Err(RadrootsRelayFetchRawBudgetExhaustion::RawEvents);
- };
- let Some(remaining_json_bytes) = self.remaining_json_bytes.checked_sub(raw_json_bytes)
- else {
- self.exhausted = Some(RadrootsRelayFetchRawBudgetExhaustion::RawJsonBytes);
- return Err(RadrootsRelayFetchRawBudgetExhaustion::RawJsonBytes);
- };
- self.remaining_events = remaining_events;
- self.remaining_json_bytes = remaining_json_bytes;
- self.exhausted = if remaining_events == 0 {
- Some(RadrootsRelayFetchRawBudgetExhaustion::RawEvents)
- } else if remaining_json_bytes == 0 {
- Some(RadrootsRelayFetchRawBudgetExhaustion::RawJsonBytes)
- } else {
- None
- };
- Ok(())
- }
-
- const fn exhaustion_reason(self) -> Option<RadrootsRelayFetchRawBudgetExhaustion> {
- self.exhausted
- }
-}
-
-fn process_relay_fetch_items(
- target_relays: Vec<String>,
- filters: Vec<RadrootsNostrFilter>,
- observed_at_ms: i64,
- max_events: usize,
- max_raw_events: usize,
- max_raw_json_bytes: usize,
- items: Vec<RadrootsRelayFetchItem>,
-) -> Result<RadrootsRelayProcessedFetch, RadrootsRelayTransportError> {
- if target_relays.is_empty() {
- return Err(RadrootsRelayTransportError::EmptyTargetSet);
- }
- let mut processed = RadrootsRelayProcessedFetch {
- target_relays,
- items: Vec::new(),
- duplicate_count: 0,
- verification_failed_count: 0,
- malformed_count: 0,
- out_of_filter_count: 0,
- skipped_over_limit_count: 0,
- eose_count: 0,
- truncated_count: 0,
- closed_count: 0,
- notice_count: 0,
- relay_outcomes: Vec::new(),
- };
- let mut raw_budget = RadrootsRelayFetchRawBudget::new(max_raw_events, max_raw_json_bytes);
- let mut accepted_events = 0usize;
- let mut seen_event_ids = BTreeSet::new();
- let mut terminal_outcomes = BTreeMap::new();
- for item in items {
- let item_relay_url = match &item {
- RadrootsRelayFetchItem::Event { relay_url, .. }
- | RadrootsRelayFetchItem::Eose { relay_url }
- | RadrootsRelayFetchItem::Truncated { relay_url, .. }
- | RadrootsRelayFetchItem::Closed { relay_url, .. }
- | RadrootsRelayFetchItem::Notice { relay_url, .. } => relay_url,
- };
- let relay_url = canonical_requested_fetch_relay(
- processed.target_relays.as_slice(),
- item_relay_url.as_str(),
- )?;
- if let Some(next) = fetch_terminal_outcome_label(&item) {
- if let Some(first) = terminal_outcomes.get(relay_url.as_str()).copied() {
- if first == next {
- return Err(
- RadrootsRelayTransportError::DuplicateFetchTerminalRelayUrl {
- url: relay_url,
- },
- );
- }
- return Err(
- RadrootsRelayTransportError::ConflictingFetchTerminalRelayUrl {
- url: relay_url,
- first,
- next,
- },
- );
- }
- terminal_outcomes.insert(relay_url.clone(), next);
- }
- match item {
- RadrootsRelayFetchItem::Event { raw_json, .. } => {
- if raw_budget.charge(raw_json.len()).is_err() {
- processed.skipped_over_limit_count += 1;
- continue;
- }
- if raw_json.len() > DEFAULT_RAW_JSON_MAX_BYTES {
- processed.verification_failed_count += 1;
- processed
- .items
- .push(RadrootsRelayProcessedFetchItem::Receipt(
- RadrootsRelayFetchEventReceipt {
- relay_url,
- event_id: None,
- inserted: false,
- duplicate: false,
- not_persisted: false,
- malformed: false,
- out_of_filter: false,
- skipped_over_limit: false,
- verification: RadrootsRelayFetchEventVerification::Failed,
- admission: RadrootsRelayFetchEventAdmission::NotEvaluated,
- admission_code: None,
- valid_stream: RadrootsRelayFetchEventValidStream::NotEvaluated,
- visibility: RadrootsRelayFetchEventVisibility::NotEvaluated,
- message: Some(format!(
- "event raw JSON exceeds {DEFAULT_RAW_JSON_MAX_BYTES} byte limit"
- )),
- },
- ));
- continue;
- }
- let parsed = RadrootsNostrEvent::from_json(raw_json.as_str());
- let Ok(raw_event) = parsed else {
- processed.malformed_count += 1;
- processed
- .items
- .push(RadrootsRelayProcessedFetchItem::Receipt(
- RadrootsRelayFetchEventReceipt {
- relay_url,
- event_id: None,
- inserted: false,
- duplicate: false,
- not_persisted: false,
- malformed: true,
- out_of_filter: false,
- skipped_over_limit: false,
- verification: RadrootsRelayFetchEventVerification::NotEvaluated,
- admission: RadrootsRelayFetchEventAdmission::NotEvaluated,
- admission_code: None,
- valid_stream: RadrootsRelayFetchEventValidStream::NotEvaluated,
- visibility: RadrootsRelayFetchEventVisibility::NotEvaluated,
- message: Some("event JSON parse failed".to_owned()),
- },
- ));
- continue;
- };
- if let Err(error) =
- RadrootsEventIngest::from_raw_json(raw_json.clone(), observed_at_ms)
- {
- processed.verification_failed_count += 1;
- processed
- .items
- .push(RadrootsRelayProcessedFetchItem::Receipt(
- RadrootsRelayFetchEventReceipt {
- relay_url,
- event_id: Some(raw_event.id.to_hex()),
- inserted: false,
- duplicate: false,
- not_persisted: false,
- malformed: false,
- out_of_filter: false,
- skipped_over_limit: false,
- verification: RadrootsRelayFetchEventVerification::Failed,
- admission: RadrootsRelayFetchEventAdmission::NotEvaluated,
- admission_code: None,
- valid_stream: RadrootsRelayFetchEventValidStream::NotEvaluated,
- visibility: RadrootsRelayFetchEventVisibility::NotEvaluated,
- message: Some(error.to_string()),
- },
- ));
- continue;
- }
- if !relay_fetch_event_matches_filters(&filters, &raw_event) {
- processed.out_of_filter_count += 1;
- processed
- .items
- .push(RadrootsRelayProcessedFetchItem::Receipt(
- RadrootsRelayFetchEventReceipt {
- relay_url,
- event_id: Some(raw_event.id.to_hex()),
- inserted: false,
- duplicate: false,
- not_persisted: false,
- malformed: false,
- out_of_filter: true,
- skipped_over_limit: false,
- verification: RadrootsRelayFetchEventVerification::Verified,
- admission: RadrootsRelayFetchEventAdmission::NotEvaluated,
- admission_code: None,
- valid_stream: RadrootsRelayFetchEventValidStream::NotEvaluated,
- visibility: RadrootsRelayFetchEventVisibility::NotEvaluated,
- message: Some("event did not match relay fetch filters".to_owned()),
- },
- ));
- continue;
- }
- let event_id = raw_event.id.to_hex();
- if !seen_event_ids.insert(event_id) {
- processed.duplicate_count += 1;
- processed
- .items
- .push(RadrootsRelayProcessedFetchItem::Duplicate(
- RadrootsRelayFetchedEvent {
- relay_url,
- event: raw_event,
- raw_json,
- observed_at_ms,
- },
- ));
- continue;
- }
- if accepted_events >= max_events {
- processed.skipped_over_limit_count += 1;
- processed
- .items
- .push(RadrootsRelayProcessedFetchItem::Receipt(
- RadrootsRelayFetchEventReceipt {
- relay_url,
- event_id: Some(raw_event.id.to_hex()),
- inserted: false,
- duplicate: false,
- not_persisted: false,
- malformed: false,
- out_of_filter: false,
- skipped_over_limit: true,
- verification: RadrootsRelayFetchEventVerification::Verified,
- admission: RadrootsRelayFetchEventAdmission::NotEvaluated,
- admission_code: None,
- valid_stream: RadrootsRelayFetchEventValidStream::NotEvaluated,
- visibility: RadrootsRelayFetchEventVisibility::NotEvaluated,
- message: Some(
- "accepted relay fetch event limit reached".to_owned(),
- ),
- },
- ));
- continue;
- }
- accepted_events += 1;
- processed
- .items
- .push(RadrootsRelayProcessedFetchItem::Accepted(
- RadrootsRelayFetchedEvent {
- relay_url,
- event: raw_event,
- raw_json,
- observed_at_ms,
- },
- ));
- }
- RadrootsRelayFetchItem::Eose { .. } => {
- processed.eose_count += 1;
- processed
- .relay_outcomes
- .push(RadrootsRelayFetchRelayOutcome {
- relay_url,
- kind: RadrootsRelayFetchOutcomeKind::Eose,
- relay_outcome: None,
- message: None,
- });
- }
- RadrootsRelayFetchItem::Truncated { message, .. } => {
- processed.truncated_count += 1;
- processed
- .relay_outcomes
- .push(RadrootsRelayFetchRelayOutcome {
- relay_url,
- kind: RadrootsRelayFetchOutcomeKind::Truncated,
- relay_outcome: None,
- message: Some(message),
- });
- }
- RadrootsRelayFetchItem::Closed { message, .. } => {
- processed.closed_count += 1;
- processed
- .relay_outcomes
- .push(RadrootsRelayFetchRelayOutcome {
- relay_url,
- kind: RadrootsRelayFetchOutcomeKind::Closed,
- relay_outcome: Some(RadrootsRelayOutcome::classify(message.as_str())),
- message: Some(message),
- });
- }
- RadrootsRelayFetchItem::Notice { message, .. } => {
- processed.notice_count += 1;
- processed
- .relay_outcomes
- .push(RadrootsRelayFetchRelayOutcome {
- relay_url,
- kind: RadrootsRelayFetchOutcomeKind::Notice,
- relay_outcome: None,
- message: Some(message),
- });
- }
- }
- }
- Ok(processed)
-}
-
-fn fetch_terminal_outcome_label(item: &RadrootsRelayFetchItem) -> Option<&'static str> {
- match item {
- RadrootsRelayFetchItem::Eose { .. } => Some("eose"),
- RadrootsRelayFetchItem::Truncated { .. } => Some("truncated"),
- RadrootsRelayFetchItem::Closed { .. } => Some("closed"),
- RadrootsRelayFetchItem::Event { .. } | RadrootsRelayFetchItem::Notice { .. } => None,
- }
-}
-
-fn canonical_requested_fetch_relay(
- target_relays: &[String],
- relay_url: &str,
-) -> Result<String, RadrootsRelayTransportError> {
- let target = Target::new(TransportId::NOSTR, relay_url).map_err(|error| {
- RadrootsRelayTransportError::InvalidFetchItemRelayUrl {
- url: relay_url.to_owned(),
- reason: error.to_string(),
- }
- })?;
- let canonical = target.uri().as_str();
- if !target_relays.iter().any(|requested| requested == canonical) {
- return Err(RadrootsRelayTransportError::UnexpectedFetchItemRelayUrl {
- url: canonical.to_owned(),
- });
- }
- Ok(canonical.to_owned())
-}
-
-fn accepted_fetch_event_receipt(
- event: &RadrootsRelayFetchedEvent,
-) -> RadrootsRelayFetchEventReceipt {
- RadrootsRelayFetchEventReceipt {
- relay_url: event.relay_url.clone(),
- event_id: Some(event.event.id.to_hex()),
- inserted: false,
- duplicate: false,
- not_persisted: false,
- malformed: false,
- out_of_filter: false,
- skipped_over_limit: false,
- verification: RadrootsRelayFetchEventVerification::Verified,
- admission: RadrootsRelayFetchEventAdmission::NotEvaluated,
- admission_code: None,
- valid_stream: RadrootsRelayFetchEventValidStream::NotEvaluated,
- visibility: RadrootsRelayFetchEventVisibility::NotEvaluated,
- message: Some("event accepted by relay fetch filters".to_owned()),
- }
-}
-
-fn duplicate_fetch_event_receipt(
- event: &RadrootsRelayFetchedEvent,
-) -> RadrootsRelayFetchEventReceipt {
- RadrootsRelayFetchEventReceipt {
- relay_url: event.relay_url.clone(),
- event_id: Some(event.event.id.to_hex()),
- inserted: false,
- duplicate: true,
- not_persisted: false,
- malformed: false,
- out_of_filter: false,
- skipped_over_limit: false,
- verification: RadrootsRelayFetchEventVerification::Verified,
- admission: RadrootsRelayFetchEventAdmission::NotEvaluated,
- admission_code: None,
- valid_stream: RadrootsRelayFetchEventValidStream::NotEvaluated,
- visibility: RadrootsRelayFetchEventVisibility::NotEvaluated,
- message: Some("event ID was already observed in this relay fetch".to_owned()),
- }
-}
-
-#[cfg_attr(coverage_nightly, coverage(off))]
-fn relay_fetch_event_matches_filters(
- filters: &[RadrootsNostrFilter],
- event: &RadrootsNostrEvent,
-) -> bool {
- !filters.is_empty()
- && filters
- .iter()
- .any(|filter| filter.match_event(event, MatchEventOptions::new()))
-}
-
-#[derive(Clone, Copy, Debug, Default)]
-pub struct RadrootsNostrClientFetchAdapter;
-
-impl RadrootsRelayFetchAdapter for RadrootsNostrClientFetchAdapter {
- #[cfg_attr(coverage_nightly, coverage(off))]
- fn fetch<'a>(
- &'a self,
- request: RadrootsRelayFetchRequest,
- ) -> BoxFuture<'a, Result<Vec<RadrootsRelayFetchItem>, RadrootsRelayTransportError>> {
- Box::pin(async move { fetch_from_nostr_relays(request).await })
- }
-}
-
-#[cfg_attr(coverage_nightly, coverage(off))]
-async fn fetch_from_nostr_relays(
- request: RadrootsRelayFetchRequest,
-) -> Result<Vec<RadrootsRelayFetchItem>, RadrootsRelayTransportError> {
- if request.filters.as_slice().is_empty() {
- return Err(RadrootsRelayTransportError::EmptyFetchFilters);
- }
- let timeout = Duration::from_millis(request.timeout_ms);
- let filters = request.filters.as_slice().to_vec();
- let mut raw_budget =
- RadrootsRelayFetchRawBudget::new(request.max_raw_events, request.max_raw_json_bytes);
- let mut items = Vec::new();
- let relay_urls = request.relay_targets.relay_strings();
- for (relay_index, relay_url) in relay_urls.iter().cloned().enumerate() {
- if let Some(reason) = raw_budget.exhaustion_reason() {
- items.extend(relay_urls[relay_index..].iter().cloned().map(|relay_url| {
- RadrootsRelayFetchItem::Truncated {
- relay_url,
- message: reason.unqueried_relay_message().to_owned(),
- }
- }));
- break;
- }
- let client = RadrootsNostrClient::new_signerless();
- if let Err(error) = client.add_read_relay(relay_url.as_str()).await {
- items.push(RadrootsRelayFetchItem::Closed {
- relay_url,
- message: error.to_string(),
- });
- continue;
- }
- let connection_output = client.try_connect(timeout).await;
- if connection_output.success.is_empty() {
- items.push(RadrootsRelayFetchItem::Closed {
- relay_url,
- message: summarize_nostr_output_failures(&connection_output.failed),
- });
- continue;
- }
- let mut closed = false;
- let mut truncated_message = None;
- for filter in filters.iter().cloned() {
- if let Some(reason) = raw_budget.exhaustion_reason() {
- truncated_message = Some(reason.current_relay_message().to_owned());
- break;
- }
- let filter_limit = filter
- .limit
- .unwrap_or(raw_budget.remaining_events)
- .min(raw_budget.remaining_events);
- match client
- .stream_events(filter.limit(filter_limit), timeout)
- .await
- {
- Ok(mut events) => {
- loop {
- if let Some(reason) = raw_budget.exhaustion_reason() {
- truncated_message = Some(reason.current_relay_message().to_owned());
- break;
- }
- let Some(event) = events.next().await else {
- break;
- };
- let raw_json = event.as_json();
- if let Err(reason) = raw_budget.charge(raw_json.len()) {
- truncated_message = Some(reason.current_relay_message().to_owned());
- break;
- }
- if raw_json.len() > DEFAULT_RAW_JSON_MAX_BYTES {
- truncated_message = Some(format!(
- "relay event raw JSON exceeds {DEFAULT_RAW_JSON_MAX_BYTES} byte limit"
- ));
- break;
- }
- items.push(RadrootsRelayFetchItem::Event {
- relay_url: relay_url.clone(),
- raw_json,
- });
- }
- if truncated_message.is_some() {
- break;
- }
- }
- Err(error) => {
- items.push(RadrootsRelayFetchItem::Closed {
- relay_url: relay_url.clone(),
- message: error.to_string(),
- });
- closed = true;
- break;
- }
- }
- }
- if let Some(message) = truncated_message {
- items.push(RadrootsRelayFetchItem::Truncated { relay_url, message });
- } else if !closed {
- items.push(unproven_relay_stream_completion(relay_url));
- }
- }
- Ok(items)
-}
-
-fn unproven_relay_stream_completion(relay_url: String) -> RadrootsRelayFetchItem {
- RadrootsRelayFetchItem::Closed {
- relay_url,
- message: "relay stream ended before EOSE could be observed".to_owned(),
- }
-}
-
-fn summarize_nostr_output_failures<K, E>(failed: &std::collections::HashMap<K, E>) -> String
-where
- K: std::fmt::Display + Eq + std::hash::Hash,
- E: std::fmt::Display,
-{
- if failed.is_empty() {
- return "no relay acknowledged the operation".to_owned();
- }
- failed
- .iter()
- .map(|(relay, error)| format!("{relay}: {error}"))
- .collect::<Vec<_>>()
- .join("; ")
-}
-
-#[derive(Clone, Default)]
-pub struct RadrootsMockRelayFetchAdapter {
- items: Arc<Mutex<Vec<RadrootsRelayFetchItem>>>,
-}
-
-impl RadrootsMockRelayFetchAdapter {
- pub fn new(items: Vec<RadrootsRelayFetchItem>) -> Self {
- Self {
- items: Arc::new(Mutex::new(items)),
- }
- }
-}
-
-impl RadrootsRelayFetchAdapter for RadrootsMockRelayFetchAdapter {
- #[cfg_attr(coverage_nightly, coverage(off))]
- fn fetch<'a>(
- &'a self,
- _request: RadrootsRelayFetchRequest,
- ) -> BoxFuture<'a, Result<Vec<RadrootsRelayFetchItem>, RadrootsRelayTransportError>> {
- Box::pin(async move { Ok(self.items.lock().map_err(fetch_item_lock_error)?.clone()) })
- }
-}
-
-#[cfg_attr(coverage_nightly, coverage(off))]
-fn fetch_item_lock_error<T>(_error: PoisonError<T>) -> RadrootsRelayTransportError {
- RadrootsRelayTransportError::Transport("fetch item lock poisoned".to_owned())
-}
-
-#[cfg(test)]
-mod tests {
- use super::{
- RadrootsNostrEvent, RadrootsRelayFetchEventAdmission, RadrootsRelayFetchEventValidStream,
- RadrootsRelayFetchEventVerification, RadrootsRelayFetchEventVisibility,
- RadrootsRelayFetchItem, RadrootsRelayFetchRawBudget, RadrootsRelayFetchRawBudgetExhaustion,
- RadrootsRelayTransportError, relay_fetch_event_matches_filters, relay_fetch_visibility,
- required_persisted_fetch_receipt_event_id, summarize_nostr_output_failures,
- unproven_relay_stream_completion,
- };
- use nostr::JsonUtil;
- use nostr::{Keys as RadrootsNostrKeys, SecretKey as RadrootsNostrSecretKey};
- use radroots_event_store::{RadrootsEventVisibility, RadrootsNip09SuppressionReason};
- use radroots_nostr::event::Kind as RadrootsNostrKind;
- use radroots_nostr::filter::Filter as RadrootsNostrFilter;
- use std::collections::HashMap;
-
- const FIXTURE_ALICE_SECRET_KEY_HEX: &str =
- "10c5304d6c9ae3a1a16f7860f1cc8f5e3a76225a2663b3a989a0d775919b7df5";
-
- fn signed_raw_event() -> RadrootsNostrEvent {
- let secret_key =
- RadrootsNostrSecretKey::from_hex(FIXTURE_ALICE_SECRET_KEY_HEX).expect("secret key");
- let keys = RadrootsNostrKeys::new(secret_key);
- let event = nostr::EventBuilder::new(nostr::Kind::TextNote, "hello")
- .sign_with_keys(&keys)
- .expect("signed event");
- RadrootsNostrEvent::from_json(event.as_json().as_str()).expect("raw event")
- }
-
- #[test]
- fn relay_fetch_filter_helper_rejects_empty_filter_set() {
- let event = signed_raw_event();
- assert!(!relay_fetch_event_matches_filters(&[], &event));
- assert!(relay_fetch_event_matches_filters(
- &[RadrootsNostrFilter::new().kind(RadrootsNostrKind::TextNote)],
- &event
- ));
- }
-
- #[test]
- fn nostr_output_failure_summary_covers_empty_and_reported_failures() {
- assert_eq!(
- summarize_nostr_output_failures::<String, String>(&HashMap::new()),
- "no relay acknowledged the operation"
- );
-
- let mut failures = HashMap::new();
- failures.insert("wss://relay.example.com".to_owned(), "timeout".to_owned());
- failures.insert("wss://relay-2.example.com".to_owned(), "denied".to_owned());
-
- let summary = summarize_nostr_output_failures(&failures);
- assert!(summary.contains("wss://relay.example.com: timeout"));
- assert!(summary.contains("wss://relay-2.example.com: denied"));
- assert!(summary.contains("; "));
- }
-
- #[test]
- fn unproven_sdk_stream_completion_never_claims_eose() {
- assert_eq!(
- unproven_relay_stream_completion("wss://relay.example".to_owned()),
- RadrootsRelayFetchItem::Closed {
- relay_url: "wss://relay.example".to_owned(),
- message: "relay stream ended before EOSE could be observed".to_owned(),
- }
- );
- }
-
- #[test]
- fn raw_fetch_budget_allows_exact_boundaries_and_sticks_after_exhaustion() {
- let mut count_budget = RadrootsRelayFetchRawBudget::new(1, 10);
- assert_eq!(count_budget.charge(1), Ok(()));
- assert_eq!(
- count_budget.exhaustion_reason(),
- Some(RadrootsRelayFetchRawBudgetExhaustion::RawEvents)
- );
- assert_eq!(
- count_budget.charge(1),
- Err(RadrootsRelayFetchRawBudgetExhaustion::RawEvents)
- );
-
- let mut byte_budget = RadrootsRelayFetchRawBudget::new(2, 5);
- assert_eq!(byte_budget.charge(5), Ok(()));
- assert_eq!(
- byte_budget.exhaustion_reason(),
- Some(RadrootsRelayFetchRawBudgetExhaustion::RawJsonBytes)
- );
- assert_eq!(
- byte_budget.charge(0),
- Err(RadrootsRelayFetchRawBudgetExhaustion::RawJsonBytes)
- );
- }
-
- #[test]
- fn raw_fetch_budget_rejects_crossing_without_partial_charge() {
- let mut budget = RadrootsRelayFetchRawBudget::new(2, 4);
- assert_eq!(
- budget.charge(usize::MAX),
- Err(RadrootsRelayFetchRawBudgetExhaustion::RawJsonBytes)
- );
- assert_eq!(budget.remaining_events, 2);
- assert_eq!(budget.remaining_json_bytes, 4);
- assert_eq!(
- budget.charge(1),
- Err(RadrootsRelayFetchRawBudgetExhaustion::RawJsonBytes)
- );
- assert_eq!(
- RadrootsRelayFetchRawBudgetExhaustion::RawEvents.current_relay_message(),
- "raw event scan limit reached before relay EOSE"
- );
- assert!(
- RadrootsRelayFetchRawBudgetExhaustion::RawJsonBytes
- .unqueried_relay_message()
- .contains("aggregate raw JSON byte limit")
- );
- }
-
- #[test]
- fn event_store_visibility_maps_without_admission_collapse() {
- let cases = [
- (
- RadrootsEventVisibility::Visible,
- RadrootsRelayFetchEventVisibility::Visible,
- ),
- (
- RadrootsEventVisibility::NotAdmitted,
- RadrootsRelayFetchEventVisibility::NotAdmitted,
- ),
- (
- RadrootsEventVisibility::NotCurrent {
- raw_head_event_id: "head".to_owned(),
- },
- RadrootsRelayFetchEventVisibility::NotCurrent,
- ),
- (
- RadrootsEventVisibility::Suppressed {
- reason: RadrootsNip09SuppressionReason::EventIdReference,
- event_reference_request_id: None,
- address_reference_request_id: None,
- address_reference_cutoff: None,
- },
- RadrootsRelayFetchEventVisibility::Suppressed,
- ),
- ];
- for (source, expected) in cases {
- assert_eq!(
- relay_fetch_visibility("event", source).expect("known visibility"),
- expected
- );
- }
- }
-
- #[test]
- fn persisted_fetch_receipts_require_an_event_id() {
- assert_eq!(
- required_persisted_fetch_receipt_event_id(Some("event")).expect("event id"),
- "event"
- );
- assert!(matches!(
- required_persisted_fetch_receipt_event_id(None),
- Err(RadrootsRelayTransportError::MissingPersistedFetchReceiptEventId)
- ));
- }
-
- #[test]
- fn event_processing_outcomes_have_stable_exhaustive_wire_values() {
- let cases = [
- serde_json::to_string(&RadrootsRelayFetchEventVerification::NotEvaluated)
- .expect("verification"),
- serde_json::to_string(&RadrootsRelayFetchEventVerification::Verified)
- .expect("verification"),
- serde_json::to_string(&RadrootsRelayFetchEventVerification::Failed)
- .expect("verification"),
- serde_json::to_string(&RadrootsRelayFetchEventAdmission::NotEvaluated)
- .expect("admission"),
- serde_json::to_string(&RadrootsRelayFetchEventAdmission::Admitted).expect("admission"),
- serde_json::to_string(&RadrootsRelayFetchEventAdmission::Unsupported)
- .expect("admission"),
- serde_json::to_string(&RadrootsRelayFetchEventAdmission::Invalid).expect("admission"),
- serde_json::to_string(&RadrootsRelayFetchEventValidStream::NotEvaluated)
- .expect("valid stream"),
- serde_json::to_string(&RadrootsRelayFetchEventValidStream::Eligible)
- .expect("valid stream"),
- serde_json::to_string(&RadrootsRelayFetchEventValidStream::Ineligible)
- .expect("valid stream"),
- serde_json::to_string(&RadrootsRelayFetchEventVisibility::NotEvaluated)
- .expect("visibility"),
- serde_json::to_string(&RadrootsRelayFetchEventVisibility::NotPersisted)
- .expect("visibility"),
- serde_json::to_string(&RadrootsRelayFetchEventVisibility::Visible).expect("visibility"),
- serde_json::to_string(&RadrootsRelayFetchEventVisibility::NotAdmitted)
- .expect("visibility"),
- serde_json::to_string(&RadrootsRelayFetchEventVisibility::NotCurrent)
- .expect("visibility"),
- serde_json::to_string(&RadrootsRelayFetchEventVisibility::Suppressed)
- .expect("visibility"),
- ];
- assert_eq!(
- cases,
- [
- "\"not_evaluated\"",
- "\"verified\"",
- "\"failed\"",
- "\"not_evaluated\"",
- "\"admitted\"",
- "\"unsupported\"",
- "\"invalid\"",
- "\"not_evaluated\"",
- "\"eligible\"",
- "\"ineligible\"",
- "\"not_evaluated\"",
- "\"not_persisted\"",
- "\"visible\"",
- "\"not_admitted\"",
- "\"not_current\"",
- "\"suppressed\"",
- ]
- );
- }
-}
diff --git a/crates/transport_nostr/src/nip11.rs b/crates/transport_nostr/src/nip11.rs
@@ -1,26 +0,0 @@
-use radroots_event::social::relay_document::RelayDocument;
-pub async fn fetch_nip11(ws_url: &str) -> Option<RelayDocument> {
- let http_url = ws_to_http(ws_url)?;
- let client = reqwest::Client::new();
- client
- .get(&http_url)
- .header("Accept", "application/nostr+json")
- .send()
- .await
- .ok()?
- .json::<RelayDocument>()
- .await
- .ok()
-}
-
-fn ws_to_http(ws: &str) -> Option<String> {
- let mut url = reqwest::Url::parse(ws).ok()?;
- let scheme = url.scheme().to_owned();
- let replacement = match scheme.as_str() {
- "wss" => "https",
- "ws" => "http",
- other => other,
- };
- url.set_scheme(replacement).ok()?;
- Some(url.into())
-}
diff --git a/crates/transport_nostr/src/outbox.rs b/crates/transport_nostr/src/outbox.rs
@@ -1,1685 +0,0 @@
-#![forbid(unsafe_code)]
-
-use crate::error::ensure_nonnegative_timestamp;
-use crate::{
- RadrootsRelayOutcome, RadrootsRelayPublishAdapter, RadrootsRelayPublishReceipt,
- RadrootsRelayPublishRelayReceipt, RadrootsRelayPublishRequest, RadrootsRelayTargetSet,
- RadrootsRelayTransportError, RadrootsRelayUrlPolicy, publish_signed_event,
-};
-use radroots_event::draft::SignedEvent;
-use radroots_event_store::{
- RadrootsEventIngest, RadrootsEventStore, RadrootsTransportObservation,
- RadrootsTransportObservationType,
-};
-use radroots_outbox::{
- RadrootsOutbox, RadrootsOutboxClaimedEvent, RadrootsOutboxDeliveryTargetRecord,
- RadrootsOutboxDeliveryTargetStatus, RadrootsOutboxEventStoreIngestReceipt,
-};
-#[cfg(test)]
-use radroots_transport::RadrootsTransportDeliveryReceipt;
-use radroots_transport::outcome::{DeliveryOutcomeKind, Retryability};
-use radroots_transport::policy::{
- SatisfactionClass, SatisfactionPolicy as SinkSatisfactionPolicy, TargetPolicy,
-};
-use radroots_transport::sink::{DeliveryPayload, DeliveryReceipt, DeliveryRequest, EventSink};
-use radroots_transport::target::{TargetFingerprint, TargetLabel, TargetScope};
-use radroots_transport::{
- RadrootsTransportDeliveryTargetStatus, RadrootsTransportError,
- RadrootsTransportSatisfactionClass, RadrootsTransportSatisfactionPolicy, Target, TargetSet,
- TransportId,
-};
-#[cfg(test)]
-use radroots_transport::{RadrootsTransportOutcome, RadrootsTransportOutcomeKind};
-
-#[derive(Clone, Debug, PartialEq, Eq)]
-pub struct RadrootsOutboxPublishPolicy {
- pub next_attempt_after_ms: i64,
- pub republish_accepted_relays: bool,
- pub relay_url_policy: RadrootsRelayUrlPolicy,
-}
-
-impl RadrootsOutboxPublishPolicy {
- pub fn new(next_attempt_after_ms: i64) -> Self {
- Self {
- next_attempt_after_ms,
- republish_accepted_relays: false,
- relay_url_policy: RadrootsRelayUrlPolicy::Public,
- }
- }
-
- pub fn republish_accepted_relays(mut self, enabled: bool) -> Self {
- self.republish_accepted_relays = enabled;
- self
- }
-
- pub fn relay_url_policy(mut self, policy: RadrootsRelayUrlPolicy) -> Self {
- self.relay_url_policy = policy;
- self
- }
-}
-
-#[derive(Clone, Debug, PartialEq, Eq)]
-pub struct RadrootsOutboxPublishReceipt {
- pub local_ingest: RadrootsOutboxEventStoreIngestReceipt,
- pub event_id: String,
- pub attempted_count: usize,
- pub accepted_count: usize,
- pub retryable_count: usize,
- pub terminal_count: usize,
- pub quorum: usize,
- pub quorum_met: bool,
- pub target_receipts: Vec<RadrootsOutboxPublishTargetReceipt>,
- pub relay_receipts: Vec<RadrootsRelayPublishRelayReceipt>,
-}
-
-#[derive(Clone, Debug, PartialEq, Eq)]
-pub struct RadrootsOutboxPublishTargetReceipt {
- pub delivery_target_id: i64,
- pub endpoint_uri: String,
- pub endpoint_fingerprint: TargetFingerprint,
- pub target_scope: Option<String>,
- pub target_label: Option<String>,
- pub attempted: bool,
- pub transport_status: RadrootsTransportDeliveryTargetStatus,
- pub outcome: RadrootsRelayOutcome,
-}
-
-pub async fn publish_claimed_outbox_event<A>(
- outbox: &RadrootsOutbox,
- event_store: &RadrootsEventStore,
- adapter: &A,
- claimed: &RadrootsOutboxClaimedEvent,
- policy: RadrootsOutboxPublishPolicy,
- now_ms: i64,
-) -> Result<RadrootsOutboxPublishReceipt, RadrootsRelayTransportError>
-where
- A: RadrootsRelayPublishAdapter,
-{
- ensure_nonnegative_timestamp("now_ms", now_ms)?;
- ensure_nonnegative_timestamp("next_attempt_after_ms", policy.next_attempt_after_ms)?;
- let signed_event = claimed.signed_event.clone().ok_or(
- RadrootsRelayTransportError::MissingSignedOutboxEvent(claimed.outbox_event_id),
- )?;
- let local_ingest = outbox
- .ingest_signed_event_local(
- event_store,
- claimed.outbox_event_id,
- claimed.claim_token.as_str(),
- now_ms,
- )
- .await?;
- let publishable = publishable_relays(outbox, claimed, policy.republish_accepted_relays).await?;
- if publishable.relays.is_empty() {
- outbox
- .complete_publish_attempt(
- claimed.outbox_event_id,
- claimed.claim_token.as_str(),
- "relay publish incomplete",
- "relay publish terminal",
- policy.next_attempt_after_ms,
- now_ms,
- )
- .await?;
- return Ok(RadrootsOutboxPublishReceipt {
- local_ingest,
- event_id: signed_event.id_str().to_owned(),
- attempted_count: 0,
- accepted_count: publishable.accepted_count,
- retryable_count: 0,
- terminal_count: 0,
- quorum: publishable.remaining_satisfaction_count,
- quorum_met: publishable.satisfied_count >= publishable.satisfaction_required_count,
- target_receipts: Vec::new(),
- relay_receipts: Vec::new(),
- });
- }
- let targets = RadrootsRelayTargetSet::new(
- unique_publishable_relay_urls(&publishable),
- policy.relay_url_policy,
- )?;
- let target_strings = targets.relay_strings();
- let active_delivery_plan_id = publishable.active_delivery_plan_id;
- let request = RadrootsRelayPublishRequest::new(signed_event.clone(), targets, now_ms)?
- .with_satisfaction_policy(RadrootsTransportSatisfactionPolicy::NoWait)
- .try_with_idempotency_key(outbox_publish_idempotency_key(
- claimed.outbox_event_id,
- claimed.attempt_count,
- signed_event.id_str(),
- active_delivery_plan_id,
- ))?;
- let publish = match publish_signed_event(adapter, request).await {
- Ok(receipt) => receipt,
- Err(RadrootsRelayTransportError::Transport(message)) => adapter_transport_failure_receipt(
- signed_event.id_str().to_owned(),
- target_strings,
- 0,
- message,
- ),
- Err(error) => return Err(error),
- };
- let target_receipts = target_receipts_from_relay_receipts(&publishable, &publish.relays);
-
- for target_receipt in &target_receipts {
- complete_outbox_delivery_target(outbox, claimed, target_receipt, now_ms).await?;
- }
-
- for relay in &publish.relays {
- if relay
- .outcome
- .to_transport_outcome()
- .status
- .counts_as_satisfied(RadrootsTransportSatisfactionClass::Accepted)
- && publishable
- .targets_for_relay(relay.relay_url.as_str())
- .next()
- .is_some()
- {
- ingest_publish_observation(
- event_store,
- &signed_event,
- relay.relay_url.as_str(),
- now_ms,
- )
- .await?;
- }
- }
-
- outbox
- .complete_publish_attempt(
- claimed.outbox_event_id,
- claimed.claim_token.as_str(),
- "relay publish incomplete",
- "relay publish terminal",
- policy.next_attempt_after_ms,
- now_ms,
- )
- .await?;
-
- Ok(RadrootsOutboxPublishReceipt {
- local_ingest,
- event_id: publish.event_id,
- attempted_count: target_receipts
- .iter()
- .filter(|receipt| receipt.attempted)
- .count(),
- accepted_count: target_receipts
- .iter()
- .filter(|receipt| receipt.outcome.counts_toward_quorum())
- .count(),
- retryable_count: target_receipts
- .iter()
- .filter(|receipt| receipt.outcome.is_retryable())
- .count(),
- terminal_count: target_receipts
- .iter()
- .filter(|receipt| receipt.outcome.is_terminal_failure())
- .count(),
- quorum: publishable.remaining_satisfaction_count,
- quorum_met: publishable.satisfied_count_after_receipts(&target_receipts)
- >= publishable.satisfaction_required_count,
- target_receipts,
- relay_receipts: publish.relays,
- })
-}
-
-pub async fn publish_claimed_outbox_event_with_transport<T>(
- outbox: &RadrootsOutbox,
- event_store: &RadrootsEventStore,
- transport: &T,
- claimed: &RadrootsOutboxClaimedEvent,
- policy: RadrootsOutboxPublishPolicy,
- now_ms: i64,
-) -> Result<RadrootsOutboxPublishReceipt, RadrootsRelayTransportError>
-where
- T: EventSink + ?Sized,
-{
- ensure_nonnegative_timestamp("now_ms", now_ms)?;
- ensure_nonnegative_timestamp("next_attempt_after_ms", policy.next_attempt_after_ms)?;
- let status = transport
- .status()
- .await
- .map_err(transport_error_to_relay_error)?;
- if status.transport_id() != TransportId::NOSTR {
- return Err(RadrootsRelayTransportError::UnexpectedTransportKind {
- expected: "nostr",
- actual: status.transport_id().canonical_label(),
- });
- }
- let signed_event = claimed.signed_event.clone().ok_or(
- RadrootsRelayTransportError::MissingSignedOutboxEvent(claimed.outbox_event_id),
- )?;
- let local_ingest = outbox
- .ingest_signed_event_local(
- event_store,
- claimed.outbox_event_id,
- claimed.claim_token.as_str(),
- now_ms,
- )
- .await?;
- let publishable = publishable_relays(outbox, claimed, policy.republish_accepted_relays).await?;
- if publishable.relays.is_empty() {
- outbox
- .complete_publish_attempt(
- claimed.outbox_event_id,
- claimed.claim_token.as_str(),
- "relay publish incomplete",
- "relay publish terminal",
- policy.next_attempt_after_ms,
- now_ms,
- )
- .await?;
- return Ok(RadrootsOutboxPublishReceipt {
- local_ingest,
- event_id: signed_event.id_str().to_owned(),
- attempted_count: 0,
- accepted_count: publishable.accepted_count,
- retryable_count: 0,
- terminal_count: 0,
- quorum: publishable.remaining_satisfaction_count,
- quorum_met: publishable.satisfied_count >= publishable.satisfaction_required_count,
- target_receipts: Vec::new(),
- relay_receipts: Vec::new(),
- });
- }
- RadrootsRelayTargetSet::new(
- unique_publishable_relay_urls(&publishable),
- policy.relay_url_policy,
- )?;
- let transport_targets = publishable_transport_targets(&publishable)?;
- let target_set = TargetSet::new(transport_targets)?;
- let request_id = outbox_publish_idempotency_key(
- claimed.outbox_event_id,
- claimed.attempt_count,
- signed_event.id_str(),
- publishable.active_delivery_plan_id,
- );
- let deadline_unix_ms =
- u64::try_from(policy.next_attempt_after_ms.max(now_ms).max(1)).map_err(|_| {
- RadrootsRelayTransportError::TransportContract(
- "outbox delivery deadline is outside the transport range".to_owned(),
- )
- })?;
- let delivery_request = DeliveryRequest::new(
- request_id,
- DeliveryPayload::new(signed_event.clone()),
- target_set,
- SinkSatisfactionPolicy::new(SatisfactionClass::Accepted, TargetPolicy::all()),
- deadline_unix_ms,
- )
- .map_err(transport_error_to_relay_error)?;
- let delivery = transport
- .deliver(delivery_request.clone())
- .await
- .map_err(transport_error_to_relay_error)?;
- delivery
- .validate_for_request(&delivery_request)
- .map_err(transport_error_to_relay_error)?;
- let relay_receipts = relay_receipts_from_delivery_receipts(&delivery)?;
- let target_receipts = target_receipts_from_delivery_receipts(&publishable, &delivery);
-
- for target_receipt in &target_receipts {
- complete_outbox_delivery_target(outbox, claimed, target_receipt, now_ms).await?;
- }
-
- for relay in &relay_receipts {
- if relay
- .outcome
- .to_transport_outcome()
- .status
- .counts_as_satisfied(RadrootsTransportSatisfactionClass::Accepted)
- && publishable
- .targets_for_relay(relay.relay_url.as_str())
- .next()
- .is_some()
- {
- ingest_publish_observation(
- event_store,
- &signed_event,
- relay.relay_url.as_str(),
- now_ms,
- )
- .await?;
- }
- }
-
- outbox
- .complete_publish_attempt(
- claimed.outbox_event_id,
- claimed.claim_token.as_str(),
- "relay publish incomplete",
- "relay publish terminal",
- policy.next_attempt_after_ms,
- now_ms,
- )
- .await?;
-
- Ok(RadrootsOutboxPublishReceipt {
- local_ingest,
- event_id: signed_event.id_str().to_owned(),
- attempted_count: target_receipts
- .iter()
- .filter(|receipt| receipt.attempted)
- .count(),
- accepted_count: target_receipts
- .iter()
- .filter(|receipt| receipt.outcome.counts_toward_quorum())
- .count(),
- retryable_count: target_receipts
- .iter()
- .filter(|receipt| receipt.outcome.is_retryable())
- .count(),
- terminal_count: target_receipts
- .iter()
- .filter(|receipt| receipt.outcome.is_terminal_failure())
- .count(),
- quorum: publishable.remaining_satisfaction_count,
- quorum_met: publishable.satisfied_count_after_receipts(&target_receipts)
- >= publishable.satisfaction_required_count,
- target_receipts,
- relay_receipts,
- })
-}
-
-fn adapter_transport_failure_receipt(
- event_id: String,
- relay_urls: Vec<String>,
- quorum: usize,
- message: String,
-) -> RadrootsRelayPublishReceipt {
- let relays = relay_urls
- .into_iter()
- .map(|relay_url| {
- RadrootsRelayPublishRelayReceipt::attempted(
- relay_url,
- RadrootsRelayOutcome::connection_failed(message.clone()),
- )
- })
- .collect::<Vec<_>>();
- RadrootsRelayPublishReceipt {
- event_id,
- attempted_count: relays.len(),
- accepted_count: 0,
- retryable_count: relays.len(),
- terminal_count: 0,
- quorum,
- quorum_met: false,
- relays,
- }
-}
-
-struct PublishableRelays {
- active_delivery_plan_id: i64,
- relays: Vec<PublishableRelay>,
- accepted_count: usize,
- satisfied_count: usize,
- satisfaction_required_count: usize,
- remaining_satisfaction_count: usize,
- satisfaction_class: RadrootsTransportSatisfactionClass,
- required_targets: Option<Vec<TargetFingerprint>>,
- #[cfg_attr(not(test), allow(dead_code))]
- remaining_required_targets: Option<Vec<TargetFingerprint>>,
-}
-
-impl PublishableRelays {
- fn targets_for_relay<'a>(
- &'a self,
- relay_url: &'a str,
- ) -> impl Iterator<Item = &'a PublishableRelay> + 'a {
- let canonical_relay_url = Target::new(TransportId::NOSTR, relay_url)
- .ok()
- .map(|target| target.uri().as_str().to_owned());
- self.relays.iter().filter(move |target| {
- canonical_relay_url
- .as_deref()
- .is_some_and(|relay_url| target.relay_url == relay_url)
- })
- }
-
- fn satisfied_count_after_receipts(
- &self,
- target_receipts: &[RadrootsOutboxPublishTargetReceipt],
- ) -> usize {
- self.satisfied_count
- + target_receipts
- .iter()
- .filter(|receipt| {
- receipt
- .transport_status
- .counts_as_satisfied(self.satisfaction_class)
- && self
- .required_targets
- .as_ref()
- .is_none_or(|required| required.contains(&receipt.endpoint_fingerprint))
- })
- .count()
- }
-}
-
-struct PublishableRelay {
- delivery_target_id: i64,
- relay_url: String,
- endpoint_fingerprint: TargetFingerprint,
- target_scope: Option<String>,
- target_label: Option<String>,
-}
-
-fn unique_publishable_relay_urls(publishable: &PublishableRelays) -> Vec<&str> {
- let mut relay_urls = Vec::new();
- for target in &publishable.relays {
- let relay_url = target.relay_url.as_str();
- if !relay_urls.contains(&relay_url) {
- relay_urls.push(relay_url);
- }
- }
- relay_urls
-}
-
-fn target_receipts_from_relay_receipts(
- publishable: &PublishableRelays,
- relay_receipts: &[RadrootsRelayPublishRelayReceipt],
-) -> Vec<RadrootsOutboxPublishTargetReceipt> {
- let mut target_receipts = Vec::new();
- for relay_receipt in relay_receipts {
- for target in publishable.targets_for_relay(relay_receipt.relay_url.as_str()) {
- target_receipts.push(RadrootsOutboxPublishTargetReceipt {
- delivery_target_id: target.delivery_target_id,
- endpoint_uri: target.relay_url.clone(),
- endpoint_fingerprint: target.endpoint_fingerprint.clone(),
- target_scope: target.target_scope.clone(),
- target_label: target.target_label.clone(),
- attempted: relay_receipt.attempted,
- transport_status: relay_receipt.outcome.to_transport_outcome().status,
- outcome: relay_receipt.outcome.clone(),
- });
- }
- }
- target_receipts
-}
-
-fn target_receipts_from_delivery_receipts(
- publishable: &PublishableRelays,
- delivery: &DeliveryReceipt,
-) -> Vec<RadrootsOutboxPublishTargetReceipt> {
- delivery
- .target_receipts()
- .iter()
- .filter_map(|receipt| {
- publishable
- .relays
- .iter()
- .find(|target| target.endpoint_fingerprint == *receipt.target().fingerprint())
- .map(|target| RadrootsOutboxPublishTargetReceipt {
- delivery_target_id: target.delivery_target_id,
- endpoint_uri: target.relay_url.clone(),
- endpoint_fingerprint: target.endpoint_fingerprint.clone(),
- target_scope: target.target_scope.clone(),
- target_label: target.target_label.clone(),
- attempted: receipt.was_attempted(),
- transport_status: delivery_target_status(receipt.outcome()),
- outcome: relay_outcome_from_delivery_outcome(receipt.outcome()),
- })
- })
- .collect()
-}
-
-async fn complete_outbox_delivery_target(
- outbox: &RadrootsOutbox,
- claimed: &RadrootsOutboxClaimedEvent,
- receipt: &RadrootsOutboxPublishTargetReceipt,
- now_ms: i64,
-) -> Result<(), RadrootsRelayTransportError> {
- match receipt.transport_status {
- RadrootsTransportDeliveryTargetStatus::Pending => {
- return Err(RadrootsRelayTransportError::TransportContract(
- "outbox publish receipt cannot complete a target with pending transport status"
- .to_owned(),
- ));
- }
- RadrootsTransportDeliveryTargetStatus::Accepted => {
- outbox
- .mark_delivery_target_accepted(
- claimed.outbox_event_id,
- claimed.claim_token.as_str(),
- receipt.delivery_target_id,
- now_ms,
- )
- .await?;
- }
- RadrootsTransportDeliveryTargetStatus::Delivered => {
- outbox
- .mark_delivery_target_delivered(
- claimed.outbox_event_id,
- claimed.claim_token.as_str(),
- receipt.delivery_target_id,
- now_ms,
- )
- .await?;
- }
- RadrootsTransportDeliveryTargetStatus::Forwarded => {
- outbox
- .mark_delivery_target_forwarded(
- claimed.outbox_event_id,
- claimed.claim_token.as_str(),
- receipt.delivery_target_id,
- now_ms,
- )
- .await?;
- }
- RadrootsTransportDeliveryTargetStatus::StoredByGateway => {
- outbox
- .mark_delivery_target_stored_by_gateway(
- claimed.outbox_event_id,
- claimed.claim_token.as_str(),
- receipt.delivery_target_id,
- now_ms,
- )
- .await?;
- }
- RadrootsTransportDeliveryTargetStatus::Seen => {
- outbox
- .mark_delivery_target_seen(
- claimed.outbox_event_id,
- claimed.claim_token.as_str(),
- receipt.delivery_target_id,
- now_ms,
- )
- .await?;
- }
- RadrootsTransportDeliveryTargetStatus::DeferredUntilImplemented => {
- outbox
- .mark_delivery_target_deferred_until_implemented(
- claimed.outbox_event_id,
- claimed.claim_token.as_str(),
- receipt.delivery_target_id,
- receipt
- .outcome
- .message
- .as_deref()
- .unwrap_or("relay publish deferred until implemented"),
- now_ms,
- )
- .await?;
- }
- RadrootsTransportDeliveryTargetStatus::SkippedPolicyDenied => {
- outbox
- .mark_delivery_target_skipped_policy_denied(
- claimed.outbox_event_id,
- claimed.claim_token.as_str(),
- receipt.delivery_target_id,
- receipt
- .outcome
- .message
- .as_deref()
- .unwrap_or("relay publish skipped by policy"),
- now_ms,
- )
- .await?;
- }
- RadrootsTransportDeliveryTargetStatus::FailedRetryable => {
- outbox
- .mark_delivery_target_failed_retryable(
- claimed.outbox_event_id,
- claimed.claim_token.as_str(),
- receipt.delivery_target_id,
- receipt
- .outcome
- .message
- .as_deref()
- .unwrap_or("relay publish retryable"),
- now_ms,
- )
- .await?;
- }
- RadrootsTransportDeliveryTargetStatus::FailedTerminal => {
- outbox
- .mark_delivery_target_failed_terminal(
- claimed.outbox_event_id,
- claimed.claim_token.as_str(),
- receipt.delivery_target_id,
- receipt
- .outcome
- .message
- .as_deref()
- .unwrap_or("relay publish terminal"),
- now_ms,
- )
- .await?;
- }
- }
- Ok(())
-}
-
-fn relay_receipts_from_delivery_receipts(
- delivery: &DeliveryReceipt,
-) -> Result<Vec<RadrootsRelayPublishRelayReceipt>, RadrootsRelayTransportError> {
- let mut relay_receipts: Vec<RadrootsRelayPublishRelayReceipt> = Vec::new();
- for receipt in delivery.target_receipts() {
- let outcome = relay_outcome_from_delivery_outcome(receipt.outcome());
- let relay_receipt = if receipt.was_attempted() {
- RadrootsRelayPublishRelayReceipt::attempted(receipt.target().uri().as_str(), outcome)
- } else {
- RadrootsRelayPublishRelayReceipt::skipped(receipt.target().uri().as_str(), outcome)
- };
- if let Some(existing) = relay_receipts
- .iter()
- .find(|existing| existing.relay_url == relay_receipt.relay_url)
- {
- if existing != &relay_receipt {
- return Err(
- RadrootsRelayTransportError::ConflictingTransportReceiptRelayUrl {
- url: relay_receipt.relay_url,
- },
- );
- }
- continue;
- }
- relay_receipts.push(relay_receipt);
- }
- Ok(relay_receipts)
-}
-
-fn delivery_target_status(
- outcome: &radroots_transport::outcome::DeliveryOutcome,
-) -> RadrootsTransportDeliveryTargetStatus {
- match outcome.kind() {
- DeliveryOutcomeKind::Accepted => RadrootsTransportDeliveryTargetStatus::Accepted,
- DeliveryOutcomeKind::Delivered => RadrootsTransportDeliveryTargetStatus::Delivered,
- DeliveryOutcomeKind::Rejected => RadrootsTransportDeliveryTargetStatus::FailedTerminal,
- DeliveryOutcomeKind::Unavailable => RadrootsTransportDeliveryTargetStatus::FailedRetryable,
- DeliveryOutcomeKind::Failed => match outcome.retryability() {
- Retryability::Retryable => RadrootsTransportDeliveryTargetStatus::FailedRetryable,
- Retryability::Terminal | Retryability::NotApplicable => {
- RadrootsTransportDeliveryTargetStatus::FailedTerminal
- }
- },
- }
-}
-
-fn relay_outcome_from_delivery_outcome(
- outcome: &radroots_transport::outcome::DeliveryOutcome,
-) -> RadrootsRelayOutcome {
- let kind = outcome
- .code()
- .and_then(relay_outcome_kind_from_code)
- .unwrap_or(match outcome.kind() {
- DeliveryOutcomeKind::Accepted => crate::RadrootsRelayOutcomeKind::Accepted,
- DeliveryOutcomeKind::Delivered => crate::RadrootsRelayOutcomeKind::Accepted,
- DeliveryOutcomeKind::Rejected => crate::RadrootsRelayOutcomeKind::Invalid,
- DeliveryOutcomeKind::Unavailable => crate::RadrootsRelayOutcomeKind::Error,
- DeliveryOutcomeKind::Failed if outcome.is_retryable() => {
- crate::RadrootsRelayOutcomeKind::Error
- }
- DeliveryOutcomeKind::Failed => crate::RadrootsRelayOutcomeKind::Invalid,
- });
- RadrootsRelayOutcome {
- kind,
- message: outcome.message().map(str::to_owned),
- }
-}
-
-#[cfg(test)]
-fn relay_outcome_from_transport_outcome(
- outcome: &RadrootsTransportOutcome,
-) -> RadrootsRelayOutcome {
- let kind = outcome
- .code
- .as_deref()
- .and_then(relay_outcome_kind_from_code)
- .unwrap_or_else(|| relay_outcome_kind_from_transport_outcome(outcome.kind));
- RadrootsRelayOutcome {
- kind,
- message: outcome.message.clone(),
- }
-}
-
-#[cfg(test)]
-fn target_receipts_from_transport_receipts(
- publishable: &PublishableRelays,
- delivery: &RadrootsTransportDeliveryReceipt,
-) -> Vec<RadrootsOutboxPublishTargetReceipt> {
- delivery
- .target_receipts()
- .iter()
- .filter_map(|receipt| {
- publishable
- .relays
- .iter()
- .find(|target| target.endpoint_fingerprint == *receipt.target.fingerprint())
- .map(|target| RadrootsOutboxPublishTargetReceipt {
- delivery_target_id: target.delivery_target_id,
- endpoint_uri: target.relay_url.clone(),
- endpoint_fingerprint: target.endpoint_fingerprint.clone(),
- target_scope: target.target_scope.clone(),
- target_label: target.target_label.clone(),
- attempted: receipt.attempted,
- transport_status: receipt.status,
- outcome: relay_outcome_from_transport_outcome(&receipt.outcome),
- })
- })
- .collect()
-}
-
-#[cfg(test)]
-fn relay_receipts_from_transport_receipts(
- delivery: &RadrootsTransportDeliveryReceipt,
-) -> Result<Vec<RadrootsRelayPublishRelayReceipt>, RadrootsRelayTransportError> {
- let mut relay_receipts: Vec<RadrootsRelayPublishRelayReceipt> = Vec::new();
- for receipt in delivery.target_receipts() {
- let outcome = relay_outcome_from_transport_outcome(&receipt.outcome);
- let relay_receipt = if receipt.attempted {
- RadrootsRelayPublishRelayReceipt::attempted(receipt.target.uri().as_str(), outcome)
- } else {
- RadrootsRelayPublishRelayReceipt::skipped(receipt.target.uri().as_str(), outcome)
- };
- if let Some(existing) = relay_receipts
- .iter()
- .find(|existing| existing.relay_url == relay_receipt.relay_url)
- {
- if existing != &relay_receipt {
- return Err(
- RadrootsRelayTransportError::ConflictingTransportReceiptRelayUrl {
- url: relay_receipt.relay_url,
- },
- );
- }
- continue;
- }
- relay_receipts.push(relay_receipt);
- }
- Ok(relay_receipts)
-}
-
-fn relay_outcome_kind_from_code(code: &str) -> Option<crate::RadrootsRelayOutcomeKind> {
- Some(match code {
- "accepted" => crate::RadrootsRelayOutcomeKind::Accepted,
- "duplicate_accepted" => crate::RadrootsRelayOutcomeKind::DuplicateAccepted,
- "blocked" => crate::RadrootsRelayOutcomeKind::Blocked,
- "rate_limited" => crate::RadrootsRelayOutcomeKind::RateLimited,
- "invalid" => crate::RadrootsRelayOutcomeKind::Invalid,
- "pow_required" => crate::RadrootsRelayOutcomeKind::PowRequired,
- "restricted" => crate::RadrootsRelayOutcomeKind::Restricted,
- "auth_required" => crate::RadrootsRelayOutcomeKind::AuthRequired,
- "muted" => crate::RadrootsRelayOutcomeKind::Muted,
- "unsupported" => crate::RadrootsRelayOutcomeKind::Unsupported,
- "payment_required" => crate::RadrootsRelayOutcomeKind::PaymentRequired,
- "error" => crate::RadrootsRelayOutcomeKind::Error,
- "timeout" => crate::RadrootsRelayOutcomeKind::Timeout,
- "connection_failed" => crate::RadrootsRelayOutcomeKind::ConnectionFailed,
- "relay_url_rejected" => crate::RadrootsRelayOutcomeKind::RelayUrlRejected,
- "skipped_already_accepted" => crate::RadrootsRelayOutcomeKind::SkippedAlreadyAccepted,
- "unknown" => crate::RadrootsRelayOutcomeKind::Unknown,
- _ => return None,
- })
-}
-
-#[cfg(test)]
-fn relay_outcome_kind_from_transport_outcome(
- kind: RadrootsTransportOutcomeKind,
-) -> crate::RadrootsRelayOutcomeKind {
- match kind {
- RadrootsTransportOutcomeKind::Accepted => crate::RadrootsRelayOutcomeKind::Accepted,
- RadrootsTransportOutcomeKind::DuplicateAccepted => {
- crate::RadrootsRelayOutcomeKind::DuplicateAccepted
- }
- RadrootsTransportOutcomeKind::Rejected => crate::RadrootsRelayOutcomeKind::Invalid,
- RadrootsTransportOutcomeKind::RouteUnavailable => {
- crate::RadrootsRelayOutcomeKind::RelayUrlRejected
- }
- RadrootsTransportOutcomeKind::PolicyDenied => crate::RadrootsRelayOutcomeKind::Restricted,
- RadrootsTransportOutcomeKind::Timeout => crate::RadrootsRelayOutcomeKind::Timeout,
- RadrootsTransportOutcomeKind::ConnectionFailed => {
- crate::RadrootsRelayOutcomeKind::ConnectionFailed
- }
- RadrootsTransportOutcomeKind::TransportUnavailable => {
- crate::RadrootsRelayOutcomeKind::Error
- }
- RadrootsTransportOutcomeKind::PayloadTooLarge => crate::RadrootsRelayOutcomeKind::Invalid,
- RadrootsTransportOutcomeKind::Delivered
- | RadrootsTransportOutcomeKind::Forwarded
- | RadrootsTransportOutcomeKind::StoredByGateway
- | RadrootsTransportOutcomeKind::Seen => crate::RadrootsRelayOutcomeKind::Accepted,
- RadrootsTransportOutcomeKind::DeferredUntilImplemented => {
- crate::RadrootsRelayOutcomeKind::Unsupported
- }
- }
-}
-
-fn publishable_transport_targets(
- publishable: &PublishableRelays,
-) -> Result<Vec<Target>, RadrootsRelayTransportError> {
- publishable
- .relays
- .iter()
- .map(|relay| {
- Target::new_with_metadata(
- TransportId::NOSTR,
- relay.relay_url.as_str(),
- relay
- .target_scope
- .as_deref()
- .map(TargetScope::parse)
- .transpose()
- .map_err(transport_error_to_relay_error)?,
- relay
- .target_label
- .as_deref()
- .map(TargetLabel::parse)
- .transpose()
- .map_err(transport_error_to_relay_error)?,
- )
- .map_err(transport_error_to_relay_error)
- })
- .collect()
-}
-
-#[cfg(test)]
-fn transport_satisfaction_policy_for_publishable(
- publishable: &PublishableRelays,
-) -> RadrootsTransportSatisfactionPolicy {
- satisfaction_policy_for_remaining_count(
- publishable.satisfaction_class,
- publishable.remaining_satisfaction_count,
- publishable.relays.len(),
- publishable.remaining_required_targets.as_deref(),
- )
-}
-
-fn transport_error_to_relay_error(error: RadrootsTransportError) -> RadrootsRelayTransportError {
- match error {
- RadrootsTransportError::UnsupportedOperation
- | RadrootsTransportError::EmptyTransportKind
- | RadrootsTransportError::InvalidTransportKind
- | RadrootsTransportError::EmptyTargetScope
- | RadrootsTransportError::InvalidTargetScope
- | RadrootsTransportError::EmptyTargetLabel
- | RadrootsTransportError::InvalidTargetLabel
- | RadrootsTransportError::InvalidSatisfactionPolicy
- | RadrootsTransportError::EmptyRequiredTargetSet
- | RadrootsTransportError::DuplicateRequiredTargetFingerprint
- | RadrootsTransportError::RequiredTargetNotRequested
- | RadrootsTransportError::EmptyDeliveryRequestId
- | RadrootsTransportError::InvalidDeliveryRequestId
- | RadrootsTransportError::InvalidDeliveryTimestamp => {
- RadrootsRelayTransportError::Transport(error.to_string())
- }
- RadrootsTransportError::EmptyTargetUri
- | RadrootsTransportError::InvalidTargetUri
- | RadrootsTransportError::EmptyTargetSet
- | RadrootsTransportError::TargetSetTooLarge
- | RadrootsTransportError::DuplicateTargetFingerprint
- | RadrootsTransportError::InvalidTargetFingerprint
- | RadrootsTransportError::EmptyFetchRequestId
- | RadrootsTransportError::InvalidFetchRequestId
- | RadrootsTransportError::InvalidFetchLimit
- | RadrootsTransportError::InvalidFetchDeadline
- | RadrootsTransportError::EmptyFetchCursor
- | RadrootsTransportError::InvalidFetchCursor
- | RadrootsTransportError::InvalidObservedAt
- | RadrootsTransportError::UnexpectedFetchProvenance
- | RadrootsTransportError::UnexpectedFetchTargetOutcome
- | RadrootsTransportError::DuplicateFetchTargetOutcome
- | RadrootsTransportError::FetchPageLimitExceeded
- | RadrootsTransportError::FetchPageRequestMismatch
- | RadrootsTransportError::InvalidDeliveryDeadline
- | RadrootsTransportError::InvalidDeliveryOutcome
- | RadrootsTransportError::UnexpectedDeliveryTargetReceipt
- | RadrootsTransportError::DuplicateDeliveryTargetReceipt
- | RadrootsTransportError::MissingDeliveryTargetReceipt
- | RadrootsTransportError::DeliveryTargetReceiptStatusMismatch
- | RadrootsTransportError::DeliveryTargetReceiptAttemptMismatch
- | RadrootsTransportError::TransportOutcomeStatusMismatch
- | RadrootsTransportError::DeliveryReceiptRequestIdMismatch
- | RadrootsTransportError::DeliveryReceiptTargetSetMismatch => {
- RadrootsRelayTransportError::TransportContract(error.to_string())
- }
- RadrootsTransportError::EmptyPayloadId
- | RadrootsTransportError::InvalidPayloadId
- | RadrootsTransportError::EmptyPayloadLabel
- | RadrootsTransportError::InvalidPayloadLabel
- | RadrootsTransportError::EmptyPayloadBytes
- | RadrootsTransportError::InvalidPayloadBytes
- | RadrootsTransportError::InvalidPayloadDigest
- | RadrootsTransportError::PayloadDigestMismatch => {
- RadrootsRelayTransportError::NostrEventJson(error.to_string())
- }
- }
-}
-
-async fn publishable_relays(
- outbox: &RadrootsOutbox,
- claimed: &RadrootsOutboxClaimedEvent,
- republish_accepted_relays: bool,
-) -> Result<PublishableRelays, RadrootsRelayTransportError> {
- let active_delivery_plan_id = claimed.active_delivery_plan_id.ok_or_else(|| {
- RadrootsRelayTransportError::Transport(format!(
- "outbox event {} has no active delivery plan for Nostr publish",
- claimed.outbox_event_id
- ))
- })?;
- let targets = outbox.delivery_targets(claimed.outbox_event_id).await?;
- let plans = outbox.delivery_plans(claimed.outbox_event_id).await?;
- let plan = plans
- .iter()
- .find(|plan| plan.delivery_plan_id == active_delivery_plan_id)
- .ok_or_else(|| {
- RadrootsRelayTransportError::Transport(format!(
- "outbox event {} active delivery plan {} was not found for Nostr publish",
- claimed.outbox_event_id, active_delivery_plan_id
- ))
- })?;
- let satisfaction_required_count = plan.required_success_count as usize;
- let required_targets = match &plan.satisfaction_policy {
- RadrootsTransportSatisfactionPolicy::RequiredTargets { targets, .. } => {
- Some(targets.clone())
- }
- RadrootsTransportSatisfactionPolicy::NoWait
- | RadrootsTransportSatisfactionPolicy::Any { .. }
- | RadrootsTransportSatisfactionPolicy::All { .. }
- | RadrootsTransportSatisfactionPolicy::Quorum { .. } => None,
- };
- let active_targets = targets
- .iter()
- .filter(|target| target.delivery_plan_id == active_delivery_plan_id)
- .collect::<Vec<_>>();
- let satisfaction_class = plan
- .satisfaction_policy
- .target_satisfaction_class()
- .unwrap_or(RadrootsTransportSatisfactionClass::Accepted);
- let satisfied_count = active_targets
- .iter()
- .filter(|target| {
- required_targets
- .as_ref()
- .is_none_or(|required| required.contains(&target.endpoint_fingerprint))
- && target
- .status
- .counts_as_transport_satisfaction(satisfaction_class)
- })
- .count();
- let remaining_satisfaction_count =
- (plan.required_success_count as usize).saturating_sub(satisfied_count);
- let remaining_required_targets = required_targets.as_ref().map(|required_targets| {
- required_targets
- .iter()
- .filter(|required| {
- !active_targets.iter().any(|target| {
- target.endpoint_fingerprint == **required
- && target
- .status
- .counts_as_transport_satisfaction(satisfaction_class)
- })
- })
- .cloned()
- .collect::<Vec<_>>()
- });
- let mut relays = Vec::new();
- let mut accepted_count = 0usize;
- for target in &active_targets {
- if !is_nostr_target(target) {
- continue;
- }
- let required_for_satisfaction = required_targets
- .as_ref()
- .is_some_and(|required| required.contains(&target.endpoint_fingerprint));
- if counts_as_accepted_for_plan(
- target.status,
- required_targets.is_some(),
- required_for_satisfaction,
- ) {
- accepted_count += 1;
- }
- let can_contribute_to_satisfaction =
- required_targets.is_none() || required_for_satisfaction;
- if remaining_satisfaction_count > 0
- && can_contribute_to_satisfaction
- && is_publishable_delivery_status(target.status, republish_accepted_relays)
- {
- relays.push(PublishableRelay {
- delivery_target_id: target.delivery_target_id,
- relay_url: target.endpoint_uri.as_str().to_owned(),
- endpoint_fingerprint: target.endpoint_fingerprint.clone(),
- target_scope: target
- .target_scope
- .as_ref()
- .map(|scope| scope.as_str().to_owned()),
- target_label: target
- .target_label
- .as_ref()
- .map(|label| label.as_str().to_owned()),
- });
- }
- }
- if required_targets.is_some() {
- let selected_relay_urls = relays
- .iter()
- .map(|relay| relay.relay_url.clone())
- .collect::<Vec<_>>();
- for target in &active_targets {
- if !is_nostr_target(target)
- || !selected_relay_urls
- .iter()
- .any(|relay_url| relay_url == target.endpoint_uri.as_str())
- || relays
- .iter()
- .any(|relay| relay.delivery_target_id == target.delivery_target_id)
- || !is_publishable_delivery_status(target.status, republish_accepted_relays)
- {
- continue;
- }
- relays.push(PublishableRelay {
- delivery_target_id: target.delivery_target_id,
- relay_url: target.endpoint_uri.as_str().to_owned(),
- endpoint_fingerprint: target.endpoint_fingerprint.clone(),
- target_scope: target
- .target_scope
- .as_ref()
- .map(|scope| scope.as_str().to_owned()),
- target_label: target
- .target_label
- .as_ref()
- .map(|label| label.as_str().to_owned()),
- });
- }
- }
- Ok(PublishableRelays {
- active_delivery_plan_id,
- relays,
- accepted_count,
- satisfied_count,
- satisfaction_required_count,
- remaining_satisfaction_count,
- satisfaction_class,
- required_targets,
- remaining_required_targets,
- })
-}
-
-fn outbox_publish_idempotency_key(
- outbox_event_id: i64,
- attempt_count: i64,
- event_id: &str,
- active_delivery_plan_id: i64,
-) -> String {
- format!(
- "radroots-nostr-outbox-{outbox_event_id}-{attempt_count}-{event_id}-{active_delivery_plan_id}"
- )
-}
-
-fn counts_as_accepted_for_plan(
- status: RadrootsOutboxDeliveryTargetStatus,
- has_required_targets: bool,
- required_for_satisfaction: bool,
-) -> bool {
- status.counts_as_transport_satisfaction(RadrootsTransportSatisfactionClass::Accepted)
- && (!has_required_targets || required_for_satisfaction)
-}
-
-fn is_publishable_delivery_status(
- status: RadrootsOutboxDeliveryTargetStatus,
- republish_accepted_relays: bool,
-) -> bool {
- status.is_ready_for_attempt()
- || (republish_accepted_relays && status == RadrootsOutboxDeliveryTargetStatus::Accepted)
-}
-
-fn is_nostr_target(target: &RadrootsOutboxDeliveryTargetRecord) -> bool {
- target.transport_kind == TransportId::NOSTR
-}
-
-#[cfg(test)]
-fn satisfaction_policy_for_remaining_count(
- satisfaction_class: RadrootsTransportSatisfactionClass,
- remaining_satisfaction_count: usize,
- target_count: usize,
- exact_required_targets: Option<&[TargetFingerprint]>,
-) -> RadrootsTransportSatisfactionPolicy {
- if let Some(targets) = exact_required_targets {
- return RadrootsTransportSatisfactionPolicy::RequiredTargets {
- class: satisfaction_class,
- targets: targets.to_vec(),
- };
- }
- if remaining_satisfaction_count >= target_count {
- return RadrootsTransportSatisfactionPolicy::All {
- class: satisfaction_class,
- };
- }
- if remaining_satisfaction_count == 0 {
- return RadrootsTransportSatisfactionPolicy::NoWait;
- }
- if remaining_satisfaction_count == 1 {
- return RadrootsTransportSatisfactionPolicy::Any {
- class: satisfaction_class,
- };
- }
- let Ok(count) = u16::try_from(remaining_satisfaction_count) else {
- return RadrootsTransportSatisfactionPolicy::All {
- class: satisfaction_class,
- };
- };
- RadrootsTransportSatisfactionPolicy::Quorum {
- class: satisfaction_class,
- threshold: count,
- }
-}
-
-async fn ingest_publish_observation(
- event_store: &RadrootsEventStore,
- signed_event: &SignedEvent,
- relay_url: &str,
- observed_at_ms: i64,
-) -> Result<(), RadrootsRelayTransportError> {
- let observation = RadrootsTransportObservation::new(
- TransportId::NOSTR,
- relay_url,
- RadrootsTransportObservationType::PublishAck,
- observed_at_ms,
- )?;
- let ingest = RadrootsEventIngest::from_signed_event(signed_event.clone(), observed_at_ms)?
- .with_observation(observation);
- event_store.ingest_event(ingest).await?;
- Ok(())
-}
-
-#[cfg(test)]
-mod tests {
- use super::{
- PublishableRelay, PublishableRelays, RadrootsOutboxDeliveryTargetStatus,
- adapter_transport_failure_receipt, counts_as_accepted_for_plan,
- is_publishable_delivery_status, publishable_transport_targets,
- relay_outcome_from_transport_outcome, relay_outcome_kind_from_code,
- relay_outcome_kind_from_transport_outcome, relay_receipts_from_transport_receipts,
- satisfaction_policy_for_remaining_count, target_receipts_from_relay_receipts,
- target_receipts_from_transport_receipts, transport_error_to_relay_error,
- transport_satisfaction_policy_for_publishable,
- };
- use crate::{
- RadrootsRelayOutcome, RadrootsRelayOutcomeKind, RadrootsRelayPublishRelayReceipt,
- RadrootsRelayTransportError,
- };
- use radroots_transport::target::TargetScope;
- use radroots_transport::{
- RadrootsTransportDeliveryReceipt, RadrootsTransportDeliveryTargetStatus,
- RadrootsTransportError, RadrootsTransportOutcome, RadrootsTransportOutcomeKind,
- RadrootsTransportSatisfactionClass, RadrootsTransportSatisfactionPolicy,
- RadrootsTransportTargetReceipt, Target, TargetSet, TransportId,
- };
-
- #[test]
- fn internal_outbox_publish_helpers_cover_policy_edges() {
- assert_eq!(
- satisfaction_policy_for_remaining_count(
- RadrootsTransportSatisfactionClass::Accepted,
- 2,
- 2,
- None
- ),
- RadrootsTransportSatisfactionPolicy::all_accepted()
- );
- assert_eq!(
- satisfaction_policy_for_remaining_count(
- RadrootsTransportSatisfactionClass::Accepted,
- 1,
- 3,
- None
- ),
- RadrootsTransportSatisfactionPolicy::any_accepted()
- );
- assert_eq!(
- satisfaction_policy_for_remaining_count(
- RadrootsTransportSatisfactionClass::Delivered,
- 2,
- 3,
- None
- ),
- RadrootsTransportSatisfactionPolicy::quorum_delivered(2)
- );
- let required_target =
- Target::new(TransportId::NOSTR, "wss://relay.example").expect("required target");
- assert_eq!(
- satisfaction_policy_for_remaining_count(
- RadrootsTransportSatisfactionClass::Delivered,
- 1,
- 3,
- Some(core::slice::from_ref(required_target.fingerprint()))
- ),
- RadrootsTransportSatisfactionPolicy::required_targets(
- RadrootsTransportSatisfactionClass::Delivered,
- vec![required_target.fingerprint().clone()]
- )
- .expect("required target policy")
- );
- assert_eq!(
- satisfaction_policy_for_remaining_count(
- RadrootsTransportSatisfactionClass::Accepted,
- usize::from(u16::MAX) + 1,
- usize::from(u16::MAX) + 2,
- None,
- ),
- RadrootsTransportSatisfactionPolicy::All {
- class: RadrootsTransportSatisfactionClass::Accepted,
- }
- );
- assert_eq!(
- satisfaction_policy_for_remaining_count(
- RadrootsTransportSatisfactionClass::Accepted,
- 0,
- 3,
- None,
- ),
- RadrootsTransportSatisfactionPolicy::NoWait
- );
-
- assert!(counts_as_accepted_for_plan(
- RadrootsOutboxDeliveryTargetStatus::Accepted,
- false,
- false,
- ));
- assert!(counts_as_accepted_for_plan(
- RadrootsOutboxDeliveryTargetStatus::Accepted,
- true,
- true,
- ));
- assert!(!counts_as_accepted_for_plan(
- RadrootsOutboxDeliveryTargetStatus::Accepted,
- true,
- false,
- ));
- assert!(!counts_as_accepted_for_plan(
- RadrootsOutboxDeliveryTargetStatus::FailedRetryable,
- false,
- false,
- ));
-
- assert!(is_publishable_delivery_status(
- RadrootsOutboxDeliveryTargetStatus::Pending,
- false,
- ));
- assert!(is_publishable_delivery_status(
- RadrootsOutboxDeliveryTargetStatus::FailedRetryable,
- false,
- ));
- assert!(is_publishable_delivery_status(
- RadrootsOutboxDeliveryTargetStatus::Accepted,
- true,
- ));
- assert!(!is_publishable_delivery_status(
- RadrootsOutboxDeliveryTargetStatus::Accepted,
- false,
- ));
- assert!(!is_publishable_delivery_status(
- RadrootsOutboxDeliveryTargetStatus::FailedTerminal,
- true,
- ));
- }
-
- #[test]
- fn outbox_publish_satisfaction_counts_use_active_transport_class() {
- let target = Target::new(TransportId::NOSTR, "wss://relay.example").expect("target");
- let publishable = PublishableRelays {
- active_delivery_plan_id: 7,
- relays: vec![PublishableRelay {
- delivery_target_id: 11,
- relay_url: target.uri().as_str().to_owned(),
- endpoint_fingerprint: target.fingerprint().clone(),
- target_scope: None,
- target_label: None,
- }],
- accepted_count: 0,
- satisfied_count: 0,
- satisfaction_required_count: 1,
- remaining_satisfaction_count: 1,
- satisfaction_class: RadrootsTransportSatisfactionClass::Delivered,
- required_targets: None,
- remaining_required_targets: None,
- };
-
- let accepted_relay_receipts = target_receipts_from_relay_receipts(
- &publishable,
- &[RadrootsRelayPublishRelayReceipt::attempted(
- target.uri().as_str(),
- RadrootsRelayOutcome::accepted(),
- )],
- );
- assert_eq!(
- accepted_relay_receipts[0].transport_status,
- RadrootsTransportDeliveryTargetStatus::Accepted
- );
- assert_eq!(
- publishable.satisfied_count_after_receipts(&accepted_relay_receipts),
- 0
- );
-
- let delivery = RadrootsTransportDeliveryReceipt::new(
- "request-1",
- TargetSet::new(vec![target.clone()]).expect("target set"),
- vec![RadrootsTransportTargetReceipt::new(
- target,
- RadrootsTransportOutcome::new(RadrootsTransportOutcomeKind::Delivered),
- )],
- )
- .expect("delivery receipt");
- let delivered_transport_receipts =
- target_receipts_from_transport_receipts(&publishable, &delivery);
- assert_eq!(
- delivered_transport_receipts[0].transport_status,
- RadrootsTransportDeliveryTargetStatus::Delivered
- );
- assert_eq!(
- publishable.satisfied_count_after_receipts(&delivered_transport_receipts),
- 1
- );
- }
-
- #[test]
- fn adapter_transport_failure_receipts_preserve_each_target() {
- let receipt = adapter_transport_failure_receipt(
- "event-1".to_owned(),
- vec![
- "wss://relay-a.example".to_owned(),
- "wss://relay-b.example".to_owned(),
- ],
- 2,
- "offline".to_owned(),
- );
-
- assert_eq!(receipt.event_id, "event-1");
- assert_eq!(receipt.attempted_count, 2);
- assert_eq!(receipt.retryable_count, 2);
- assert_eq!(receipt.terminal_count, 0);
- assert_eq!(receipt.quorum, 2);
- assert!(!receipt.quorum_met);
- assert!(receipt.relays.iter().all(|relay| relay.attempted));
- }
-
- #[test]
- fn transport_outcomes_preserve_relay_semantics() {
- let cases = [
- (
- RadrootsTransportOutcomeKind::Accepted,
- RadrootsRelayOutcomeKind::Accepted,
- ),
- (
- RadrootsTransportOutcomeKind::DuplicateAccepted,
- RadrootsRelayOutcomeKind::DuplicateAccepted,
- ),
- (
- RadrootsTransportOutcomeKind::Delivered,
- RadrootsRelayOutcomeKind::Accepted,
- ),
- (
- RadrootsTransportOutcomeKind::Forwarded,
- RadrootsRelayOutcomeKind::Accepted,
- ),
- (
- RadrootsTransportOutcomeKind::StoredByGateway,
- RadrootsRelayOutcomeKind::Accepted,
- ),
- (
- RadrootsTransportOutcomeKind::Seen,
- RadrootsRelayOutcomeKind::Accepted,
- ),
- (
- RadrootsTransportOutcomeKind::DeferredUntilImplemented,
- RadrootsRelayOutcomeKind::Unsupported,
- ),
- (
- RadrootsTransportOutcomeKind::Rejected,
- RadrootsRelayOutcomeKind::Invalid,
- ),
- (
- RadrootsTransportOutcomeKind::RouteUnavailable,
- RadrootsRelayOutcomeKind::RelayUrlRejected,
- ),
- (
- RadrootsTransportOutcomeKind::PayloadTooLarge,
- RadrootsRelayOutcomeKind::Invalid,
- ),
- (
- RadrootsTransportOutcomeKind::PolicyDenied,
- RadrootsRelayOutcomeKind::Restricted,
- ),
- (
- RadrootsTransportOutcomeKind::Timeout,
- RadrootsRelayOutcomeKind::Timeout,
- ),
- (
- RadrootsTransportOutcomeKind::ConnectionFailed,
- RadrootsRelayOutcomeKind::ConnectionFailed,
- ),
- (
- RadrootsTransportOutcomeKind::TransportUnavailable,
- RadrootsRelayOutcomeKind::Error,
- ),
- ];
- for (transport_kind, relay_kind) in cases {
- assert_eq!(
- relay_outcome_kind_from_transport_outcome(transport_kind),
- relay_kind
- );
- let outcome = RadrootsTransportOutcome::new(transport_kind)
- .with_message(format!("{transport_kind:?}"));
- let relay_outcome = relay_outcome_from_transport_outcome(&outcome);
- assert_eq!(relay_outcome.kind, relay_kind);
- assert_eq!(relay_outcome.message, outcome.message);
- }
-
- let code_cases = [
- ("accepted", RadrootsRelayOutcomeKind::Accepted),
- (
- "duplicate_accepted",
- RadrootsRelayOutcomeKind::DuplicateAccepted,
- ),
- ("blocked", RadrootsRelayOutcomeKind::Blocked),
- ("rate_limited", RadrootsRelayOutcomeKind::RateLimited),
- ("invalid", RadrootsRelayOutcomeKind::Invalid),
- ("pow_required", RadrootsRelayOutcomeKind::PowRequired),
- ("restricted", RadrootsRelayOutcomeKind::Restricted),
- ("auth_required", RadrootsRelayOutcomeKind::AuthRequired),
- ("muted", RadrootsRelayOutcomeKind::Muted),
- ("unsupported", RadrootsRelayOutcomeKind::Unsupported),
- (
- "payment_required",
- RadrootsRelayOutcomeKind::PaymentRequired,
- ),
- ("error", RadrootsRelayOutcomeKind::Error),
- ("timeout", RadrootsRelayOutcomeKind::Timeout),
- (
- "connection_failed",
- RadrootsRelayOutcomeKind::ConnectionFailed,
- ),
- (
- "relay_url_rejected",
- RadrootsRelayOutcomeKind::RelayUrlRejected,
- ),
- (
- "skipped_already_accepted",
- RadrootsRelayOutcomeKind::SkippedAlreadyAccepted,
- ),
- ("unknown", RadrootsRelayOutcomeKind::Unknown),
- ];
- for (code, relay_kind) in code_cases {
- assert_eq!(relay_outcome_kind_from_code(code), Some(relay_kind));
- assert_eq!(
- relay_outcome_from_transport_outcome(
- &RadrootsTransportOutcome::new(RadrootsTransportOutcomeKind::Rejected)
- .with_code(code)
- )
- .kind,
- relay_kind
- );
- }
- assert_eq!(relay_outcome_kind_from_code("unrecognized"), None);
- assert_eq!(
- relay_outcome_from_transport_outcome(
- &RadrootsTransportOutcome::new(RadrootsTransportOutcomeKind::Seen)
- .with_code("unrecognized")
- )
- .kind,
- RadrootsRelayOutcomeKind::Accepted
- );
-
- for kind in [
- RadrootsRelayOutcomeKind::RateLimited,
- RadrootsRelayOutcomeKind::Error,
- RadrootsRelayOutcomeKind::Unknown,
- ] {
- assert_eq!(
- kind.transport_outcome_kind(),
- RadrootsTransportOutcomeKind::TransportUnavailable
- );
- }
- }
-
- #[test]
- fn transport_target_and_error_adapters_preserve_contract_categories() {
- let target = Target::new(TransportId::NOSTR, "wss://relay.example").expect("target");
- let publishable = PublishableRelays {
- active_delivery_plan_id: 7,
- relays: vec![PublishableRelay {
- delivery_target_id: 11,
- relay_url: target.uri().as_str().to_owned(),
- endpoint_fingerprint: target.fingerprint().clone(),
- target_scope: Some("foodshed.west".to_owned()),
- target_label: Some("primary relay".to_owned()),
- }],
- accepted_count: 0,
- satisfied_count: 0,
- satisfaction_required_count: 1,
- remaining_satisfaction_count: 1,
- satisfaction_class: RadrootsTransportSatisfactionClass::Accepted,
- required_targets: None,
- remaining_required_targets: None,
- };
- let targets = publishable_transport_targets(&publishable).expect("transport targets");
- assert_eq!(targets.len(), 1);
- assert_eq!(targets[0].scope().expect("scope").as_str(), "foodshed.west");
- assert_eq!(targets[0].label().expect("label").as_str(), "primary relay");
- assert_eq!(
- transport_satisfaction_policy_for_publishable(&publishable),
- RadrootsTransportSatisfactionPolicy::all_accepted()
- );
-
- let mut invalid = publishable;
- invalid.relays[0].target_scope = Some("bad scope".to_owned());
- assert!(matches!(
- publishable_transport_targets(&invalid),
- Err(RadrootsRelayTransportError::Transport(_))
- ));
- invalid.relays[0].target_scope = Some("foodshed.west".to_owned());
- invalid.relays[0].target_label = Some("bad\0label".to_owned());
- assert!(matches!(
- publishable_transport_targets(&invalid),
- Err(RadrootsRelayTransportError::Transport(_))
- ));
- invalid.relays[0].target_label = Some("primary relay".to_owned());
- invalid.relays[0].relay_url = "not-a-relay".to_owned();
- assert!(matches!(
- publishable_transport_targets(&invalid),
- Err(RadrootsRelayTransportError::TransportContract(_))
- ));
-
- let generic_errors = [
- RadrootsTransportError::UnsupportedOperation,
- RadrootsTransportError::EmptyTransportKind,
- RadrootsTransportError::InvalidTransportKind,
- RadrootsTransportError::EmptyTargetScope,
- RadrootsTransportError::InvalidTargetScope,
- RadrootsTransportError::EmptyTargetLabel,
- RadrootsTransportError::InvalidTargetLabel,
- RadrootsTransportError::InvalidSatisfactionPolicy,
- RadrootsTransportError::EmptyRequiredTargetSet,
- RadrootsTransportError::DuplicateRequiredTargetFingerprint,
- ];
- for error in generic_errors {
- assert!(matches!(
- transport_error_to_relay_error(error),
- RadrootsRelayTransportError::Transport(_)
- ));
- }
- let target_errors = [
- RadrootsTransportError::EmptyTargetUri,
- RadrootsTransportError::InvalidTargetUri,
- RadrootsTransportError::EmptyTargetSet,
- RadrootsTransportError::DuplicateTargetFingerprint,
- RadrootsTransportError::InvalidTargetFingerprint,
- ];
- for error in target_errors {
- assert!(matches!(
- transport_error_to_relay_error(error),
- RadrootsRelayTransportError::TransportContract(_)
- ));
- }
- let payload_errors = [
- RadrootsTransportError::EmptyPayloadId,
- RadrootsTransportError::InvalidPayloadId,
- RadrootsTransportError::EmptyPayloadLabel,
- RadrootsTransportError::InvalidPayloadLabel,
- RadrootsTransportError::EmptyPayloadBytes,
- RadrootsTransportError::InvalidPayloadBytes,
- RadrootsTransportError::InvalidPayloadDigest,
- RadrootsTransportError::PayloadDigestMismatch,
- ];
- for error in payload_errors {
- assert!(matches!(
- transport_error_to_relay_error(error),
- RadrootsRelayTransportError::NostrEventJson(_)
- ));
- }
-
- let unknown =
- Target::new(TransportId::NOSTR, "wss://unknown.example").expect("unknown target");
- let delivery = RadrootsTransportDeliveryReceipt::new(
- "unknown",
- TargetSet::new(vec![unknown.clone()]).expect("target set"),
- vec![RadrootsTransportTargetReceipt::new(
- unknown,
- RadrootsTransportOutcome::new(RadrootsTransportOutcomeKind::Accepted),
- )],
- )
- .expect("delivery receipt");
- assert!(target_receipts_from_transport_receipts(&invalid, &delivery).is_empty());
- assert_eq!(
- relay_receipts_from_transport_receipts(&delivery)
- .expect("relay receipts")
- .len(),
- 1
- );
-
- let west = Target::new_with_metadata(
- TransportId::NOSTR,
- "wss://scoped.example",
- Some(TargetScope::parse("foodshed.west").expect("west scope")),
- None,
- )
- .expect("west target");
- let east = Target::new_with_metadata(
- TransportId::NOSTR,
- "wss://scoped.example",
- Some(TargetScope::parse("foodshed.east").expect("east scope")),
- None,
- )
- .expect("east target");
- let scoped_relay_uri = west.uri().as_str().to_owned();
- let conflicting = RadrootsTransportDeliveryReceipt::new(
- "conflicting",
- TargetSet::new(vec![west.clone(), east.clone()]).expect("scoped target set"),
- vec![
- RadrootsTransportTargetReceipt::new(
- west,
- RadrootsTransportOutcome::new(RadrootsTransportOutcomeKind::Accepted),
- ),
- RadrootsTransportTargetReceipt::new(
- east,
- RadrootsTransportOutcome::new(RadrootsTransportOutcomeKind::Timeout),
- ),
- ],
- )
- .expect("conflicting delivery receipt");
- assert!(matches!(
- relay_receipts_from_transport_receipts(&conflicting),
- Err(RadrootsRelayTransportError::ConflictingTransportReceiptRelayUrl { url })
- if url == scoped_relay_uri
- ));
- }
-}
diff --git a/crates/transport_nostr/src/outcome.rs b/crates/transport_nostr/src/outcome.rs
@@ -1,248 +0,0 @@
-#![forbid(unsafe_code)]
-
-use radroots_transport::outcome::DeliveryOutcome;
-use radroots_transport::{RadrootsTransportOutcome, RadrootsTransportOutcomeKind};
-use serde::{Deserialize, Serialize};
-
-#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
-pub enum RadrootsRelayOutcomeKind {
- Accepted,
- DuplicateAccepted,
- Blocked,
- RateLimited,
- Invalid,
- PowRequired,
- Restricted,
- AuthRequired,
- Muted,
- Unsupported,
- PaymentRequired,
- Error,
- Timeout,
- ConnectionFailed,
- RelayUrlRejected,
- SkippedAlreadyAccepted,
- Unknown,
-}
-
-impl RadrootsRelayOutcomeKind {
- pub fn as_str(self) -> &'static str {
- match self {
- Self::Accepted => "accepted",
- Self::DuplicateAccepted => "duplicate_accepted",
- Self::Blocked => "blocked",
- Self::RateLimited => "rate_limited",
- Self::Invalid => "invalid",
- Self::PowRequired => "pow_required",
- Self::Restricted => "restricted",
- Self::AuthRequired => "auth_required",
- Self::Muted => "muted",
- Self::Unsupported => "unsupported",
- Self::PaymentRequired => "payment_required",
- Self::Error => "error",
- Self::Timeout => "timeout",
- Self::ConnectionFailed => "connection_failed",
- Self::RelayUrlRejected => "relay_url_rejected",
- Self::SkippedAlreadyAccepted => "skipped_already_accepted",
- Self::Unknown => "unknown",
- }
- }
-
- pub fn counts_toward_quorum(self) -> bool {
- matches!(
- self,
- Self::Accepted | Self::DuplicateAccepted | Self::SkippedAlreadyAccepted
- )
- }
-
- pub fn is_retryable(self) -> bool {
- matches!(
- self,
- Self::RateLimited
- | Self::PowRequired
- | Self::AuthRequired
- | Self::Error
- | Self::Timeout
- | Self::ConnectionFailed
- | Self::Unknown
- )
- }
-
- pub fn is_terminal_failure(self) -> bool {
- matches!(
- self,
- Self::Blocked
- | Self::Invalid
- | Self::Restricted
- | Self::Muted
- | Self::Unsupported
- | Self::PaymentRequired
- | Self::RelayUrlRejected
- )
- }
-
- pub fn transport_outcome_kind(self) -> RadrootsTransportOutcomeKind {
- match self {
- Self::Accepted => RadrootsTransportOutcomeKind::Accepted,
- Self::DuplicateAccepted | Self::SkippedAlreadyAccepted => {
- RadrootsTransportOutcomeKind::DuplicateAccepted
- }
- Self::Blocked | Self::Invalid | Self::Restricted | Self::Muted | Self::Unsupported => {
- RadrootsTransportOutcomeKind::Rejected
- }
- Self::RelayUrlRejected => RadrootsTransportOutcomeKind::RouteUnavailable,
- Self::PaymentRequired | Self::PowRequired | Self::AuthRequired => {
- RadrootsTransportOutcomeKind::PolicyDenied
- }
- Self::RateLimited | Self::Error | Self::Unknown => {
- RadrootsTransportOutcomeKind::TransportUnavailable
- }
- Self::Timeout => RadrootsTransportOutcomeKind::Timeout,
- Self::ConnectionFailed => RadrootsTransportOutcomeKind::ConnectionFailed,
- }
- }
-}
-
-#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
-pub struct RadrootsRelayOutcome {
- pub kind: RadrootsRelayOutcomeKind,
- pub message: Option<String>,
-}
-
-impl RadrootsRelayOutcome {
- pub fn accepted() -> Self {
- Self {
- kind: RadrootsRelayOutcomeKind::Accepted,
- message: None,
- }
- }
-
- pub fn duplicate_accepted(message: impl Into<String>) -> Self {
- Self {
- kind: RadrootsRelayOutcomeKind::DuplicateAccepted,
- message: Some(message.into()),
- }
- }
-
- pub fn connection_failed(message: impl Into<String>) -> Self {
- Self {
- kind: RadrootsRelayOutcomeKind::ConnectionFailed,
- message: Some(message.into()),
- }
- }
-
- pub fn unknown(message: impl Into<String>) -> Self {
- Self {
- kind: RadrootsRelayOutcomeKind::Unknown,
- message: Some(message.into()),
- }
- }
-
- pub fn timeout(message: impl Into<String>) -> Self {
- Self {
- kind: RadrootsRelayOutcomeKind::Timeout,
- message: Some(message.into()),
- }
- }
-
- pub fn relay_url_rejected(message: impl Into<String>) -> Self {
- Self {
- kind: RadrootsRelayOutcomeKind::RelayUrlRejected,
- message: Some(message.into()),
- }
- }
-
- pub fn skipped_already_accepted(message: impl Into<String>) -> Self {
- Self {
- kind: RadrootsRelayOutcomeKind::SkippedAlreadyAccepted,
- message: Some(message.into()),
- }
- }
-
- pub fn classify(message: impl AsRef<str>) -> Self {
- let message = message.as_ref().trim();
- let lower = message.to_ascii_lowercase();
- let kind = if lower.starts_with("duplicate:") {
- RadrootsRelayOutcomeKind::DuplicateAccepted
- } else if lower.starts_with("blocked:") {
- RadrootsRelayOutcomeKind::Blocked
- } else if lower.starts_with("rate-limited:") {
- RadrootsRelayOutcomeKind::RateLimited
- } else if lower.starts_with("invalid:") {
- RadrootsRelayOutcomeKind::Invalid
- } else if lower.starts_with("pow:") {
- RadrootsRelayOutcomeKind::PowRequired
- } else if lower.starts_with("restricted:") {
- RadrootsRelayOutcomeKind::Restricted
- } else if lower.starts_with("auth-required:") {
- RadrootsRelayOutcomeKind::AuthRequired
- } else if lower.starts_with("mute:") {
- RadrootsRelayOutcomeKind::Muted
- } else if lower.starts_with("unsupported:") {
- RadrootsRelayOutcomeKind::Unsupported
- } else if lower.starts_with("payment-required:") {
- RadrootsRelayOutcomeKind::PaymentRequired
- } else if lower.starts_with("error:") {
- RadrootsRelayOutcomeKind::Error
- } else if lower.starts_with("timeout:") {
- RadrootsRelayOutcomeKind::Timeout
- } else {
- RadrootsRelayOutcomeKind::Unknown
- };
- Self {
- kind,
- message: Some(message.to_owned()),
- }
- }
-
- pub fn counts_toward_quorum(&self) -> bool {
- self.kind.counts_toward_quorum()
- }
-
- pub fn is_retryable(&self) -> bool {
- self.kind.is_retryable()
- }
-
- pub fn is_terminal_failure(&self) -> bool {
- self.kind.is_terminal_failure()
- }
-
- pub fn to_transport_outcome(&self) -> RadrootsTransportOutcome {
- let mut outcome = RadrootsTransportOutcome::new(self.kind.transport_outcome_kind())
- .with_code(self.kind.as_str());
- if let Some(message) = &self.message {
- outcome = outcome.with_message(message.clone());
- }
- outcome
- }
-
- pub(crate) fn to_delivery_outcome(&self) -> DeliveryOutcome {
- let outcome = match self.kind {
- RadrootsRelayOutcomeKind::Accepted
- | RadrootsRelayOutcomeKind::DuplicateAccepted
- | RadrootsRelayOutcomeKind::SkippedAlreadyAccepted => DeliveryOutcome::accepted(),
- RadrootsRelayOutcomeKind::Blocked
- | RadrootsRelayOutcomeKind::Invalid
- | RadrootsRelayOutcomeKind::Restricted
- | RadrootsRelayOutcomeKind::Muted
- | RadrootsRelayOutcomeKind::Unsupported
- | RadrootsRelayOutcomeKind::PaymentRequired
- | RadrootsRelayOutcomeKind::RelayUrlRejected => DeliveryOutcome::rejected(),
- RadrootsRelayOutcomeKind::RateLimited
- | RadrootsRelayOutcomeKind::PowRequired
- | RadrootsRelayOutcomeKind::AuthRequired
- | RadrootsRelayOutcomeKind::Error
- | RadrootsRelayOutcomeKind::Timeout
- | RadrootsRelayOutcomeKind::ConnectionFailed
- | RadrootsRelayOutcomeKind::Unknown => DeliveryOutcome::unavailable(),
- };
- if let Some(message) = &self.message {
- outcome
- .clone()
- .with_detail(self.kind.as_str(), message.clone())
- .unwrap_or(outcome)
- } else {
- outcome
- }
- }
-}
diff --git a/crates/transport_nostr/src/publish.rs b/crates/transport_nostr/src/publish.rs
@@ -1,1120 +0,0 @@
-#![forbid(unsafe_code)]
-
-use crate::error::ensure_nonnegative_timestamp;
-use crate::{RadrootsRelayOutcome, RadrootsRelayTargetSet, RadrootsRelayTransportError};
-#[cfg(feature = "client")]
-use core::time::Duration;
-use futures::future::BoxFuture;
-use radroots_event::draft::SignedEvent;
-use radroots_transport::capability::{Availability, Maturity, SinkCapabilities};
-use radroots_transport::outcome::DeliveryOutcome;
-use radroots_transport::sink::{
- DeliveryReceipt, DeliveryRequest, DeliveryTargetReceipt, EventSink, SinkStatus,
-};
-use radroots_transport::{
- RadrootsTransportError, RadrootsTransportPayload, RadrootsTransportSatisfactionPolicy, Target,
- TransportId,
-};
-use serde::{Deserialize, Serialize};
-use std::collections::{BTreeMap, BTreeSet};
-use std::sync::{Arc, Mutex, PoisonError};
-
-#[cfg(feature = "client")]
-use crate::RadrootsNostrClient;
-use crate::RadrootsRelayOutcomeKind;
-#[cfg(feature = "client")]
-use nostr::JsonUtil;
-#[cfg(feature = "client")]
-use radroots_nostr::event::Event as RadrootsNostrEvent;
-
-#[cfg(feature = "client")]
-const RELAY_CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
-pub const RADROOTS_RELAY_PUBLISH_IDEMPOTENCY_KEY_MAX_BYTES: usize = 256;
-
-#[derive(Clone, Debug, PartialEq, Eq)]
-pub struct RadrootsRelayPublishRequest {
- signed_event: SignedEvent,
- targets: RadrootsRelayTargetSet,
- satisfaction_policy: RadrootsTransportSatisfactionPolicy,
- idempotency_key: Option<String>,
- now_ms: i64,
-}
-
-impl RadrootsRelayPublishRequest {
- pub fn new(
- signed_event: SignedEvent,
- targets: RadrootsRelayTargetSet,
- now_ms: i64,
- ) -> Result<Self, RadrootsRelayTransportError> {
- ensure_nonnegative_timestamp("now_ms", now_ms)?;
- Ok(Self {
- signed_event,
- targets,
- satisfaction_policy: RadrootsTransportSatisfactionPolicy::all_accepted(),
- idempotency_key: None,
- now_ms,
- })
- }
-
- pub fn with_satisfaction_policy(
- mut self,
- satisfaction_policy: RadrootsTransportSatisfactionPolicy,
- ) -> Self {
- self.satisfaction_policy = satisfaction_policy;
- self
- }
-
- pub fn try_with_idempotency_key(
- mut self,
- idempotency_key: impl Into<String>,
- ) -> Result<Self, RadrootsRelayTransportError> {
- let idempotency_key = idempotency_key.into();
- validate_publish_idempotency_key(idempotency_key.as_str())?;
- self.idempotency_key = Some(idempotency_key);
- Ok(self)
- }
-
- pub fn signed_event(&self) -> &SignedEvent {
- &self.signed_event
- }
-
- pub fn targets(&self) -> &RadrootsRelayTargetSet {
- &self.targets
- }
-
- pub fn satisfaction_policy(&self) -> &RadrootsTransportSatisfactionPolicy {
- &self.satisfaction_policy
- }
-
- pub fn idempotency_key(&self) -> Option<&str> {
- self.idempotency_key.as_deref()
- }
-
- pub fn now_ms(&self) -> i64 {
- self.now_ms
- }
-
- fn validate(&self) -> Result<(), RadrootsRelayTransportError> {
- ensure_nonnegative_timestamp("now_ms", self.now_ms)?;
- if let Some(idempotency_key) = self.idempotency_key.as_deref() {
- validate_publish_idempotency_key(idempotency_key)?;
- }
- let target_count = self.targets.len();
- self.satisfaction_policy
- .required_target_count(target_count)?;
- if let Some(required_targets) = self.satisfaction_policy.required_target_fingerprints() {
- let requested = self
- .targets
- .relays()
- .iter()
- .map(|relay| Target::new(TransportId::NOSTR, relay.as_str()))
- .collect::<Result<Vec<_>, _>>()?;
- for required in required_targets {
- if !requested
- .iter()
- .any(|target| target.fingerprint() == required)
- {
- return Err(RadrootsRelayTransportError::RequiredTargetNotRequested {
- fingerprint: required.as_str().to_owned(),
- });
- }
- }
- }
- Ok(())
- }
-}
-
-fn validate_publish_idempotency_key(value: &str) -> Result<(), RadrootsRelayTransportError> {
- let reason = if value.is_empty() {
- Some("key must not be empty")
- } else if value != value.trim() {
- Some("key must not have surrounding whitespace")
- } else if value.chars().any(char::is_control) {
- Some("key must not contain control characters")
- } else if value.len() > RADROOTS_RELAY_PUBLISH_IDEMPOTENCY_KEY_MAX_BYTES {
- Some("key exceeds the UTF-8 byte limit")
- } else {
- None
- };
- if let Some(reason) = reason {
- return Err(RadrootsRelayTransportError::InvalidIdempotencyKey {
- reason,
- actual_bytes: value.len(),
- max_bytes: RADROOTS_RELAY_PUBLISH_IDEMPOTENCY_KEY_MAX_BYTES,
- });
- }
- Ok(())
-}
-
-#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
-pub struct RadrootsRelayPublishRelayReceipt {
- pub relay_url: String,
- pub outcome: RadrootsRelayOutcome,
- pub attempted: bool,
-}
-
-impl RadrootsRelayPublishRelayReceipt {
- pub fn attempted(relay_url: impl Into<String>, outcome: RadrootsRelayOutcome) -> Self {
- Self {
- relay_url: relay_url.into(),
- outcome,
- attempted: true,
- }
- }
-
- pub fn skipped(relay_url: impl Into<String>, outcome: RadrootsRelayOutcome) -> Self {
- Self {
- relay_url: relay_url.into(),
- outcome,
- attempted: false,
- }
- }
-}
-
-#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
-pub struct RadrootsRelayPublishReceipt {
- pub event_id: String,
- pub attempted_count: usize,
- pub accepted_count: usize,
- pub retryable_count: usize,
- pub terminal_count: usize,
- pub quorum: usize,
- pub quorum_met: bool,
- pub relays: Vec<RadrootsRelayPublishRelayReceipt>,
-}
-
-pub trait RadrootsRelayPublishAdapter: Send + Sync {
- fn publish<'a>(
- &'a self,
- request: RadrootsRelayPublishRequest,
- ) -> BoxFuture<'a, Result<Vec<RadrootsRelayPublishRelayReceipt>, RadrootsRelayTransportError>>;
-}
-
-pub fn verified_signed_event_payload(
- signed_event: &SignedEvent,
-) -> Result<RadrootsTransportPayload, RadrootsTransportError> {
- RadrootsTransportPayload::unchecked_signed_event_json(
- signed_event.id_str(),
- signed_event.raw_json(),
- )
-}
-
-impl<A> RadrootsRelayPublishAdapter for &A
-where
- A: RadrootsRelayPublishAdapter + ?Sized,
-{
- fn publish<'a>(
- &'a self,
- request: RadrootsRelayPublishRequest,
- ) -> BoxFuture<'a, Result<Vec<RadrootsRelayPublishRelayReceipt>, RadrootsRelayTransportError>>
- {
- (*self).publish(request)
- }
-}
-
-#[derive(Clone)]
-pub struct RadrootsNostrTransport<A> {
- adapter: A,
- status: SinkStatus,
-}
-
-impl<A> RadrootsNostrTransport<A> {
- pub fn new(adapter: A) -> Self {
- Self {
- adapter,
- status: SinkStatus::new(
- TransportId::NOSTR,
- true,
- Maturity::Stable,
- Availability::Available,
- SinkCapabilities::DELIVER,
- "ready",
- ),
- }
- }
-
- pub fn with_status(mut self, status: SinkStatus) -> Self {
- self.status = status;
- self
- }
-
- pub fn adapter(&self) -> &A {
- &self.adapter
- }
-}
-
-impl<A> EventSink for RadrootsNostrTransport<A>
-where
- A: RadrootsRelayPublishAdapter,
-{
- fn status(
- &self,
- ) -> radroots_transport::BoxFuture<'_, Result<SinkStatus, RadrootsTransportError>> {
- Box::pin(async move { Ok(self.status.clone()) })
- }
-
- fn deliver(
- &self,
- request: DeliveryRequest,
- ) -> radroots_transport::BoxFuture<'_, Result<DeliveryReceipt, RadrootsTransportError>> {
- Box::pin(async move {
- let signed_event = request.payload().event().clone();
- let targets = relay_targets_from_transport_targets(request.target_set().targets())?;
- let legacy_attempt_time_ms = i64::try_from(request.deadline_unix_ms())
- .map_err(|_| RadrootsTransportError::InvalidDeliveryDeadline)?;
- let publish_request =
- RadrootsRelayPublishRequest::new(signed_event, targets, legacy_attempt_time_ms)
- .map_err(nostr_error_to_transport_error)?
- .with_satisfaction_policy(RadrootsTransportSatisfactionPolicy::NoWait)
- .try_with_idempotency_key(request.request_id().as_str())
- .map_err(nostr_error_to_transport_error)?;
- let relay_receipts = match publish_signed_event(&self.adapter, publish_request).await {
- Ok(receipt) => receipt.relays,
- Err(RadrootsRelayTransportError::Transport(message)) => {
- return DeliveryReceipt::for_request(
- &request,
- transport_failure_target_receipts(
- request.target_set().targets(),
- message.as_str(),
- )?,
- );
- }
- Err(error) => return Err(nostr_error_to_transport_error(error)),
- };
- DeliveryReceipt::for_request(
- &request,
- target_receipts_from_relay_receipts(
- request.target_set().targets(),
- relay_receipts.as_slice(),
- )?,
- )
- })
- }
-}
-
-fn nostr_error_to_transport_error(error: RadrootsRelayTransportError) -> RadrootsTransportError {
- match error {
- RadrootsRelayTransportError::TransportContract(_) => {
- RadrootsTransportError::InvalidPayloadBytes
- }
- RadrootsRelayTransportError::ConflictingTransportReceiptRelayUrl { .. } => {
- RadrootsTransportError::DuplicateDeliveryTargetReceipt
- }
- RadrootsRelayTransportError::UnexpectedTransportKind { .. }
- | RadrootsRelayTransportError::DuplicateFetchTerminalRelayUrl { .. }
- | RadrootsRelayTransportError::ConflictingFetchTerminalRelayUrl { .. } => {
- RadrootsTransportError::InvalidTransportKind
- }
- RadrootsRelayTransportError::RelayUrlParse { .. }
- | RadrootsRelayTransportError::WsRequiresLocalhostPolicy { .. }
- | RadrootsRelayTransportError::UnsupportedRelayScheme { .. }
- | RadrootsRelayTransportError::EmptyRelayHost { .. }
- | RadrootsRelayTransportError::RelayUrlUserinfo { .. }
- | RadrootsRelayTransportError::RelayUrlQueryOrFragment { .. }
- | RadrootsRelayTransportError::RelayUrlForbiddenDestination { .. }
- | RadrootsRelayTransportError::RelayUrlResolvedForbiddenDestination { .. }
- | RadrootsRelayTransportError::RelayUrlResolvedNoAddresses { .. }
- | RadrootsRelayTransportError::EmptyTargetSet
- | RadrootsRelayTransportError::DuplicateRelayUrl { .. }
- | RadrootsRelayTransportError::InvalidFetchItemRelayUrl { .. }
- | RadrootsRelayTransportError::UnexpectedFetchItemRelayUrl { .. }
- | RadrootsRelayTransportError::InvalidPublishReceiptRelayUrl { .. }
- | RadrootsRelayTransportError::UnexpectedPublishReceiptRelayUrl { .. }
- | RadrootsRelayTransportError::DuplicatePublishReceiptRelayUrl { .. }
- | RadrootsRelayTransportError::InvalidPublishReceiptAttemptState { .. } => {
- RadrootsTransportError::InvalidTargetUri
- }
- #[cfg(feature = "client")]
- RadrootsRelayTransportError::NostrEvent(_)
- | RadrootsRelayTransportError::NostrEventJson(_)
- | RadrootsRelayTransportError::Json(_) => RadrootsTransportError::InvalidPayloadBytes,
- #[cfg(not(feature = "client"))]
- RadrootsRelayTransportError::NostrEventJson(_) | RadrootsRelayTransportError::Json(_) => {
- RadrootsTransportError::InvalidPayloadBytes
- }
- #[cfg(feature = "client")]
- RadrootsRelayTransportError::Client(_)
- | RadrootsRelayTransportError::ClientDatabase(_)
- | RadrootsRelayTransportError::ClientConfig(_)
- | RadrootsRelayTransportError::EventNotFound(_) => {
- RadrootsTransportError::InvalidTransportKind
- }
- RadrootsRelayTransportError::Transport(_) => RadrootsTransportError::InvalidTransportKind,
- RadrootsRelayTransportError::EmptyFetchFilters
- | RadrootsRelayTransportError::InvalidFetchLimit { .. }
- | RadrootsRelayTransportError::FetchLimitTooLarge { .. }
- | RadrootsRelayTransportError::InvalidTimestamp { .. }
- | RadrootsRelayTransportError::InvalidIdempotencyKey { .. }
- | RadrootsRelayTransportError::RequiredTargetNotRequested { .. } => {
- RadrootsTransportError::InvalidTransportKind
- }
- #[cfg(feature = "storage")]
- RadrootsRelayTransportError::EventStore(_)
- | RadrootsRelayTransportError::Outbox(_)
- | RadrootsRelayTransportError::MissingSignedOutboxEvent(_)
- | RadrootsRelayTransportError::MissingPersistedFetchReceiptEventId
- | RadrootsRelayTransportError::MissingStoredEventVisibility { .. }
- | RadrootsRelayTransportError::UnsupportedStoredEventVisibility { .. } => {
- RadrootsTransportError::InvalidTransportKind
- }
- }
-}
-
-#[cfg(test)]
-mod contract_tests {
- use super::nostr_error_to_transport_error;
- use crate::RadrootsRelayTransportError;
- use radroots_transport::RadrootsTransportError;
-
- #[test]
- fn relay_errors_map_to_stable_transport_contract_categories() {
- assert_eq!(
- nostr_error_to_transport_error(RadrootsRelayTransportError::TransportContract(
- "contract".to_owned(),
- )),
- RadrootsTransportError::InvalidPayloadBytes
- );
- assert_eq!(
- nostr_error_to_transport_error(
- RadrootsRelayTransportError::ConflictingTransportReceiptRelayUrl {
- url: "wss://relay.example".to_owned(),
- },
- ),
- RadrootsTransportError::DuplicateDeliveryTargetReceipt
- );
- for error in [
- RadrootsRelayTransportError::UnexpectedTransportKind {
- expected: "nostr",
- actual: "reticulum".to_owned(),
- },
- RadrootsRelayTransportError::DuplicateFetchTerminalRelayUrl {
- url: "wss://relay.example".to_owned(),
- },
- RadrootsRelayTransportError::ConflictingFetchTerminalRelayUrl {
- url: "wss://relay.example".to_owned(),
- first: "eose",
- next: "closed",
- },
- ] {
- assert_eq!(
- nostr_error_to_transport_error(error),
- RadrootsTransportError::InvalidTransportKind
- );
- }
-
- let target_errors = [
- RadrootsRelayTransportError::RelayUrlParse {
- url: "bad".to_owned(),
- reason: "parse".to_owned(),
- },
- RadrootsRelayTransportError::WsRequiresLocalhostPolicy {
- url: "ws://relay.example".to_owned(),
- },
- RadrootsRelayTransportError::UnsupportedRelayScheme {
- url: "https://relay.example".to_owned(),
- scheme: "https".to_owned(),
- },
- RadrootsRelayTransportError::EmptyRelayHost {
- url: "wss://".to_owned(),
- },
- RadrootsRelayTransportError::RelayUrlUserinfo {
- url: "wss://user@relay.example".to_owned(),
- },
- RadrootsRelayTransportError::RelayUrlQueryOrFragment {
- url: "wss://relay.example?x=1".to_owned(),
- },
- RadrootsRelayTransportError::RelayUrlForbiddenDestination {
- url: "wss://127.0.0.1".to_owned(),
- reason: "loopback".to_owned(),
- },
- RadrootsRelayTransportError::RelayUrlResolvedForbiddenDestination {
- url: "wss://relay.example".to_owned(),
- address: "127.0.0.1".to_owned(),
- reason: "loopback".to_owned(),
- },
- RadrootsRelayTransportError::RelayUrlResolvedNoAddresses {
- url: "wss://relay.example".to_owned(),
- },
- RadrootsRelayTransportError::EmptyTargetSet,
- RadrootsRelayTransportError::DuplicateRelayUrl {
- url: "wss://relay.example".to_owned(),
- },
- RadrootsRelayTransportError::InvalidFetchItemRelayUrl {
- url: "bad".to_owned(),
- reason: "parse".to_owned(),
- },
- RadrootsRelayTransportError::UnexpectedFetchItemRelayUrl {
- url: "wss://other.example".to_owned(),
- },
- RadrootsRelayTransportError::InvalidPublishReceiptRelayUrl {
- url: "bad".to_owned(),
- reason: "parse".to_owned(),
- },
- RadrootsRelayTransportError::UnexpectedPublishReceiptRelayUrl {
- url: "wss://other.example".to_owned(),
- },
- RadrootsRelayTransportError::DuplicatePublishReceiptRelayUrl {
- url: "wss://relay.example".to_owned(),
- },
- ];
- for error in target_errors {
- assert_eq!(
- nostr_error_to_transport_error(error),
- RadrootsTransportError::InvalidTargetUri
- );
- }
-
- assert_eq!(
- nostr_error_to_transport_error(RadrootsRelayTransportError::NostrEventJson(
- "event".to_owned(),
- )),
- RadrootsTransportError::InvalidPayloadBytes
- );
- let json_error = serde_json::from_str::<serde_json::Value>("{").expect_err("invalid json");
- assert_eq!(
- nostr_error_to_transport_error(RadrootsRelayTransportError::Json(json_error)),
- RadrootsTransportError::InvalidPayloadBytes
- );
- assert_eq!(
- nostr_error_to_transport_error(RadrootsRelayTransportError::Transport(
- "offline".to_owned(),
- )),
- RadrootsTransportError::InvalidTransportKind
- );
- assert_eq!(
- nostr_error_to_transport_error(RadrootsRelayTransportError::EmptyFetchFilters),
- RadrootsTransportError::InvalidTransportKind
- );
- assert_eq!(
- nostr_error_to_transport_error(RadrootsRelayTransportError::InvalidFetchLimit {
- field: "max_events",
- }),
- RadrootsTransportError::InvalidTransportKind
- );
- assert_eq!(
- nostr_error_to_transport_error(RadrootsRelayTransportError::FetchLimitTooLarge {
- field: "max_events",
- max: 1_000,
- actual: 1_001,
- }),
- RadrootsTransportError::InvalidTransportKind
- );
- assert_eq!(
- nostr_error_to_transport_error(RadrootsRelayTransportError::InvalidTimestamp {
- field: "now_ms",
- value: -1,
- }),
- RadrootsTransportError::InvalidTransportKind
- );
- assert_eq!(
- nostr_error_to_transport_error(RadrootsRelayTransportError::InvalidIdempotencyKey {
- reason: "empty",
- actual_bytes: 0,
- max_bytes: 256,
- }),
- RadrootsTransportError::InvalidTransportKind
- );
- assert_eq!(
- nostr_error_to_transport_error(
- RadrootsRelayTransportError::RequiredTargetNotRequested {
- fingerprint: "sha256:missing".to_owned(),
- },
- ),
- RadrootsTransportError::InvalidTransportKind
- );
-
- #[cfg(feature = "storage")]
- {
- assert_eq!(
- nostr_error_to_transport_error(RadrootsRelayTransportError::EventStore(
- radroots_event_store::RadrootsEventStoreError::MissingEvent(
- "missing".to_owned(),
- ),
- )),
- RadrootsTransportError::InvalidTransportKind
- );
- assert_eq!(
- nostr_error_to_transport_error(RadrootsRelayTransportError::Outbox(
- radroots_outbox::RadrootsOutboxError::EventNotFound(1),
- )),
- RadrootsTransportError::InvalidTransportKind
- );
- assert_eq!(
- nostr_error_to_transport_error(
- RadrootsRelayTransportError::MissingSignedOutboxEvent(1),
- ),
- RadrootsTransportError::InvalidTransportKind
- );
- assert_eq!(
- nostr_error_to_transport_error(
- RadrootsRelayTransportError::MissingPersistedFetchReceiptEventId,
- ),
- RadrootsTransportError::InvalidTransportKind
- );
- assert_eq!(
- nostr_error_to_transport_error(
- RadrootsRelayTransportError::MissingStoredEventVisibility {
- event_id: "missing".to_owned(),
- },
- ),
- RadrootsTransportError::InvalidTransportKind
- );
- assert_eq!(
- nostr_error_to_transport_error(
- RadrootsRelayTransportError::UnsupportedStoredEventVisibility {
- event_id: "unsupported".to_owned(),
- },
- ),
- RadrootsTransportError::InvalidTransportKind
- );
- }
- }
-}
-
-fn relay_targets_from_transport_targets(
- targets: &[Target],
-) -> Result<RadrootsRelayTargetSet, RadrootsTransportError> {
- let mut relays = Vec::new();
- for target in targets {
- if target.kind() != &TransportId::NOSTR {
- return Err(RadrootsTransportError::InvalidTargetUri);
- }
- let policy = if target.uri().as_str().starts_with("ws://") {
- crate::RadrootsRelayUrlPolicy::Localhost
- } else {
- crate::RadrootsRelayUrlPolicy::Public
- };
- let relay = crate::RelayUrl::parse(target.uri().as_str(), policy)
- .map_err(nostr_error_to_transport_error)?;
- if !relays.contains(&relay) {
- relays.push(relay);
- }
- }
- RadrootsRelayTargetSet::from_urls(relays).map_err(nostr_error_to_transport_error)
-}
-
-fn target_receipts_from_relay_receipts(
- targets: &[Target],
- relay_receipts: &[RadrootsRelayPublishRelayReceipt],
-) -> Result<Vec<DeliveryTargetReceipt>, RadrootsTransportError> {
- targets
- .iter()
- .cloned()
- .map(|target| {
- let relay_receipt = relay_receipts
- .iter()
- .find(|receipt| relay_receipt_matches_target(receipt, &target));
- match relay_receipt {
- Some(receipt) if receipt.attempted => Ok(DeliveryTargetReceipt::attempted(
- target,
- receipt.outcome.to_delivery_outcome(),
- )),
- Some(receipt) => {
- DeliveryTargetReceipt::skipped(target, receipt.outcome.to_delivery_outcome())
- }
- None => DeliveryTargetReceipt::skipped(
- target,
- DeliveryOutcome::unavailable()
- .with_detail("missing_receipt", "relay adapter omitted target receipt")?,
- ),
- }
- })
- .collect()
-}
-
-fn transport_failure_target_receipts(
- targets: &[Target],
- message: &str,
-) -> Result<Vec<DeliveryTargetReceipt>, RadrootsTransportError> {
- targets
- .iter()
- .cloned()
- .map(|target| {
- Ok(DeliveryTargetReceipt::attempted(
- target,
- DeliveryOutcome::unavailable()
- .with_detail("connection_failed", message.to_owned())
- .unwrap_or_else(|_| DeliveryOutcome::unavailable()),
- ))
- })
- .collect()
-}
-
-pub async fn publish_signed_event<A>(
- adapter: &A,
- request: RadrootsRelayPublishRequest,
-) -> Result<RadrootsRelayPublishReceipt, RadrootsRelayTransportError>
-where
- A: RadrootsRelayPublishAdapter,
-{
- request.validate()?;
- let event_id = request.signed_event.id_str().to_owned();
- let satisfaction_policy = request.satisfaction_policy.clone();
- let requested_relays = request.targets.relay_strings();
- let target_count = request.targets.len();
- let quorum = satisfaction_policy.required_target_count(target_count)?;
- let relays =
- normalize_publish_receipts(requested_relays.as_slice(), adapter.publish(request).await?)?;
- let attempted_count = relays.iter().filter(|receipt| receipt.attempted).count();
- let accepted_count = relays
- .iter()
- .filter(|receipt| relay_receipt_counts_toward_quorum(receipt))
- .count();
- let retryable_count = relays
- .iter()
- .filter(|receipt| receipt.outcome.is_retryable())
- .count();
- let terminal_count = relays
- .iter()
- .filter(|receipt| receipt.outcome.is_terminal_failure())
- .count();
- let quorum_met = relay_publish_satisfies_policy(&satisfaction_policy, target_count, &relays)?;
- Ok(RadrootsRelayPublishReceipt {
- event_id,
- attempted_count,
- accepted_count,
- retryable_count,
- terminal_count,
- quorum,
- quorum_met,
- relays,
- })
-}
-
-fn normalize_publish_receipts(
- requested_relays: &[String],
- receipts: Vec<RadrootsRelayPublishRelayReceipt>,
-) -> Result<Vec<RadrootsRelayPublishRelayReceipt>, RadrootsRelayTransportError> {
- let requested = requested_relays.iter().cloned().collect::<BTreeSet<_>>();
- let mut by_relay = BTreeMap::new();
- for mut receipt in receipts {
- let target =
- Target::new(TransportId::NOSTR, receipt.relay_url.as_str()).map_err(|error| {
- RadrootsRelayTransportError::InvalidPublishReceiptRelayUrl {
- url: receipt.relay_url.clone(),
- reason: error.to_string(),
- }
- })?;
- let canonical = target.uri().as_str().to_owned();
- if !requested.contains(&canonical) {
- return Err(
- RadrootsRelayTransportError::UnexpectedPublishReceiptRelayUrl { url: canonical },
- );
- }
- receipt.relay_url.clone_from(&canonical);
- if (!receipt.attempted
- && matches!(
- receipt.outcome.kind,
- RadrootsRelayOutcomeKind::Accepted | RadrootsRelayOutcomeKind::DuplicateAccepted
- ))
- || (receipt.attempted
- && receipt.outcome.kind == RadrootsRelayOutcomeKind::SkippedAlreadyAccepted)
- {
- return Err(
- RadrootsRelayTransportError::InvalidPublishReceiptAttemptState { url: canonical },
- );
- }
- if by_relay.insert(canonical.clone(), receipt).is_some() {
- return Err(
- RadrootsRelayTransportError::DuplicatePublishReceiptRelayUrl { url: canonical },
- );
- }
- }
- Ok(requested_relays
- .iter()
- .map(|relay_url| {
- by_relay.remove(relay_url).unwrap_or_else(|| {
- RadrootsRelayPublishRelayReceipt::skipped(
- relay_url,
- RadrootsRelayOutcome::unknown("relay adapter omitted target receipt"),
- )
- })
- })
- .collect())
-}
-
-fn relay_receipt_counts_toward_quorum(receipt: &RadrootsRelayPublishRelayReceipt) -> bool {
- receipt.outcome.counts_toward_quorum()
- && (receipt.attempted
- || receipt.outcome.kind == RadrootsRelayOutcomeKind::SkippedAlreadyAccepted)
-}
-
-fn relay_publish_satisfies_policy(
- policy: &RadrootsTransportSatisfactionPolicy,
- target_count: usize,
- relays: &[RadrootsRelayPublishRelayReceipt],
-) -> Result<bool, RadrootsRelayTransportError> {
- match policy {
- RadrootsTransportSatisfactionPolicy::NoWait => Ok(true),
- RadrootsTransportSatisfactionPolicy::Any { class }
- | RadrootsTransportSatisfactionPolicy::All { class }
- | RadrootsTransportSatisfactionPolicy::Quorum { class, .. } => {
- let satisfied_count = relays
- .iter()
- .filter(|receipt| {
- relay_receipt_counts_toward_quorum(receipt)
- && receipt
- .outcome
- .to_transport_outcome()
- .status
- .counts_as_satisfied(*class)
- })
- .count();
- Ok(policy.is_satisfied_by(target_count, satisfied_count)?)
- }
- RadrootsTransportSatisfactionPolicy::RequiredTargets { class, targets } => {
- policy.required_target_count(target_count)?;
- let mut satisfied_required_targets = BTreeSet::new();
- for receipt in relays {
- let target = Target::new(TransportId::NOSTR, &receipt.relay_url)?;
- if targets.contains(target.fingerprint())
- && relay_receipt_counts_toward_quorum(receipt)
- && receipt
- .outcome
- .to_transport_outcome()
- .status
- .counts_as_satisfied(*class)
- {
- satisfied_required_targets.insert(target.fingerprint().clone());
- }
- }
- Ok(targets
- .iter()
- .all(|target| satisfied_required_targets.contains(target)))
- }
- }
-}
-
-fn relay_receipt_matches_target(
- receipt: &RadrootsRelayPublishRelayReceipt,
- target: &Target,
-) -> bool {
- Target::new(TransportId::NOSTR, receipt.relay_url.as_str())
- .is_ok_and(|receipt_target| receipt_target.uri() == target.uri())
-}
-
-#[derive(Clone, Default)]
-pub struct RadrootsMockRelayPublishAdapter {
- outcomes: BTreeMap<String, RadrootsRelayOutcome>,
- captured_raw_events: Arc<Mutex<Vec<String>>>,
-}
-
-impl RadrootsMockRelayPublishAdapter {
- pub fn new() -> Self {
- Self::default()
- }
-
- pub fn with_outcome(
- mut self,
- relay_url: impl Into<String>,
- outcome: RadrootsRelayOutcome,
- ) -> Self {
- self.outcomes.insert(relay_url.into(), outcome);
- self
- }
-
- pub fn captured_raw_events(&self) -> Vec<String> {
- self.captured_raw_events
- .lock()
- .expect("captured raw event lock")
- .clone()
- }
-}
-
-impl RadrootsRelayPublishAdapter for RadrootsMockRelayPublishAdapter {
- fn publish<'a>(
- &'a self,
- request: RadrootsRelayPublishRequest,
- ) -> BoxFuture<'a, Result<Vec<RadrootsRelayPublishRelayReceipt>, RadrootsRelayTransportError>>
- {
- Box::pin(async move {
- self.captured_raw_events
- .lock()
- .map_err(captured_raw_event_lock_error)?
- .push(request.signed_event.raw_json().to_owned());
- Ok(request
- .targets
- .relays()
- .iter()
- .map(|relay| {
- let outcome = self
- .outcomes
- .get(relay.as_str())
- .cloned()
- .unwrap_or_else(RadrootsRelayOutcome::accepted);
- RadrootsRelayPublishRelayReceipt::attempted(relay.as_str(), outcome)
- })
- .collect())
- })
- }
-}
-
-#[cfg_attr(coverage_nightly, coverage(off))]
-fn captured_raw_event_lock_error<T>(_error: PoisonError<T>) -> RadrootsRelayTransportError {
- RadrootsRelayTransportError::Transport("captured raw event lock poisoned".to_owned())
-}
-
-#[cfg(feature = "client")]
-#[derive(Clone)]
-pub struct RadrootsNostrClientPublishAdapter {
- client: RadrootsNostrClient,
-}
-
-#[cfg(feature = "client")]
-impl RadrootsNostrClientPublishAdapter {
- #[cfg_attr(coverage_nightly, coverage(off))]
- pub fn new(client: RadrootsNostrClient) -> Self {
- Self { client }
- }
-}
-
-#[cfg(feature = "client")]
-impl RadrootsRelayPublishAdapter for RadrootsNostrClientPublishAdapter {
- #[cfg_attr(coverage_nightly, coverage(off))]
- fn publish<'a>(
- &'a self,
- request: RadrootsRelayPublishRequest,
- ) -> BoxFuture<'a, Result<Vec<RadrootsRelayPublishRelayReceipt>, RadrootsRelayTransportError>>
- {
- Box::pin(async move {
- let event = RadrootsNostrEvent::from_json(request.signed_event.raw_json())
- .map_err(|error| RadrootsRelayTransportError::NostrEventJson(error.to_string()))?;
- ensure_raw_event_matches_signed_event(&event, &request.signed_event)?;
- let target_strings = request.targets.relay_strings();
- for relay_url in &target_strings {
- self.client
- .add_write_relay(relay_url.as_str())
- .await
- .map_err(|error| RadrootsRelayTransportError::Transport(error.to_string()))?;
- }
- let connection_output = self.client.try_connect(RELAY_CONNECT_TIMEOUT).await;
- let target_url_set = target_strings
- .iter()
- .map(|relay_url| relay_url.trim_end_matches('/').to_owned())
- .collect::<BTreeSet<_>>();
- let connected_strings = self
- .client
- .relays()
- .await
- .into_values()
- .filter(|relay| relay.is_connected())
- .map(|relay| relay.url().to_string())
- .filter(|relay_url| target_url_set.contains(relay_url.trim_end_matches('/')))
- .collect::<Vec<_>>();
- let connection_failures = connection_output
- .failed
- .iter()
- .map(|(relay, reason)| {
- (
- relay.to_string().trim_end_matches('/').to_owned(),
- reason.clone(),
- )
- })
- .collect::<BTreeMap<_, _>>();
- if connected_strings.is_empty() {
- return Ok(target_strings
- .into_iter()
- .map(|relay_url| {
- let target_url = relay_url.trim_end_matches('/');
- let reason = connection_failures
- .get(target_url)
- .cloned()
- .unwrap_or_else(|| "relay did not connect".to_owned());
- RadrootsRelayPublishRelayReceipt::attempted(
- relay_url,
- RadrootsRelayOutcome::connection_failed(reason),
- )
- })
- .collect());
- }
- let output = match self.client.send_event_to(connected_strings, &event).await {
- Ok(output) => output,
- Err(error) => {
- let message = error.to_string();
- return Ok(target_strings
- .into_iter()
- .map(|relay_url| {
- RadrootsRelayPublishRelayReceipt::attempted(
- relay_url,
- RadrootsRelayOutcome::connection_failed(message.clone()),
- )
- })
- .collect());
- }
- };
- let mut receipts = Vec::new();
- for relay_url in &target_strings {
- let target_url = relay_url.trim_end_matches('/');
- let success = output
- .success
- .iter()
- .any(|success_url| success_url.to_string().trim_end_matches('/') == target_url);
- if success {
- receipts.push(RadrootsRelayPublishRelayReceipt::attempted(
- relay_url,
- RadrootsRelayOutcome {
- kind: RadrootsRelayOutcomeKind::Accepted,
- message: Some(
- "nostr-relay-pool-success-ok-message-unavailable".to_owned(),
- ),
- },
- ));
- continue;
- }
- if let Some(reason) = connection_failures.get(target_url) {
- receipts.push(RadrootsRelayPublishRelayReceipt::attempted(
- relay_url,
- RadrootsRelayOutcome::connection_failed(reason.clone()),
- ));
- continue;
- }
- let failed = output.failed.iter().find_map(|(failed_url, message)| {
- if failed_url.to_string().trim_end_matches('/') == target_url {
- Some(message.clone())
- } else {
- None
- }
- });
- let outcome = failed
- .map(RadrootsRelayOutcome::classify)
- .unwrap_or_else(|| {
- RadrootsRelayOutcome::classify("error: relay output omitted target")
- });
- receipts.push(RadrootsRelayPublishRelayReceipt::attempted(
- relay_url, outcome,
- ));
- }
- Ok(receipts)
- })
- }
-}
-
-#[cfg(feature = "client")]
-fn ensure_raw_event_matches_signed_event(
- event: &RadrootsNostrEvent,
- signed_event: &SignedEvent,
-) -> Result<(), RadrootsRelayTransportError> {
- let mismatches = [
- ("id", event.id.to_hex(), signed_event.id_str().to_owned()),
- (
- "pubkey",
- event.pubkey.to_hex(),
- signed_event.pubkey().to_hex().to_owned(),
- ),
- (
- "created_at",
- event.created_at.as_secs().to_string(),
- signed_event.created_at().to_string(),
- ),
- (
- "kind",
- (event.kind.as_u16() as u32).to_string(),
- signed_event.kind().to_string(),
- ),
- (
- "content",
- event.content.clone(),
- signed_event.content().to_owned(),
- ),
- (
- "sig",
- event.sig.to_string(),
- signed_event.sig_str().to_owned(),
- ),
- ];
- for (field, raw, wrapped) in mismatches {
- if raw != wrapped {
- return Err(RadrootsRelayTransportError::NostrEventJson(format!(
- "raw event JSON {field} does not match signed event {field}"
- )));
- }
- }
- let raw_tags = event
- .tags
- .iter()
- .map(|tag| tag.as_slice().to_vec())
- .collect::<Vec<_>>();
- if raw_tags != signed_event.tags_as_vec() {
- return Err(RadrootsRelayTransportError::NostrEventJson(
- "raw event JSON tags do not match signed event tags".to_owned(),
- ));
- }
- Ok(())
-}
-
-#[cfg(all(test, feature = "client"))]
-mod tests {
- use super::{RadrootsNostrEvent, ensure_raw_event_matches_signed_event};
- use nostr::JsonUtil;
- use nostr::{Keys as RadrootsNostrKeys, SecretKey as RadrootsNostrSecretKey};
- use radroots_event::draft::{EventDraft, SignedEvent};
- use radroots_event::envelope::kind::KIND_GEOCHAT;
- use radroots_event::wire::Nip01EventWire;
- use radroots_nostr::signing::sign_frozen_draft;
-
- const FIXTURE_ALICE_SECRET_KEY_HEX: &str =
- "10c5304d6c9ae3a1a16f7860f1cc8f5e3a76225a2663b3a989a0d775919b7df5";
- const FIXTURE_ALICE_PUBLIC_KEY_HEX: &str =
- "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df";
-
- fn signed_post(content: &str) -> (RadrootsNostrEvent, SignedEvent) {
- let secret_key =
- RadrootsNostrSecretKey::from_hex(FIXTURE_ALICE_SECRET_KEY_HEX).expect("secret key");
- let keys = RadrootsNostrKeys::new(secret_key);
- let draft = EventDraft::new(
- "radroots.social.geochat.v1",
- KIND_GEOCHAT,
- 1_700_000_000,
- vec![vec!["t".to_owned(), "soil".to_owned()]],
- content,
- FIXTURE_ALICE_PUBLIC_KEY_HEX,
- )
- .expect("draft");
- let signed_event = sign_frozen_draft(&keys, &draft).expect("signed event");
- let raw_event = RadrootsNostrEvent::from_json(signed_event.raw_json()).expect("raw event");
- (raw_event, signed_event)
- }
-
- fn assert_mismatch(raw_event: RadrootsNostrEvent, signed_event: &SignedEvent) {
- assert!(ensure_raw_event_matches_signed_event(&raw_event, signed_event).is_err());
- }
-
- fn raw_event_from_wire(wire: Nip01EventWire) -> RadrootsNostrEvent {
- RadrootsNostrEvent::from_json(serde_json::to_string(&wire).expect("raw event json"))
- .expect("raw event")
- }
-
- #[test]
- fn raw_event_match_guard_accepts_exact_event_and_rejects_field_mismatches() {
- let (raw_event, signed_event) = signed_post("matched");
- ensure_raw_event_matches_signed_event(&raw_event, &signed_event).expect("matching event");
-
- let mut wire = signed_event.wire().clone();
- wire.id = "00".repeat(32);
- assert_mismatch(raw_event_from_wire(wire), &signed_event);
-
- let mut wire = signed_event.wire().clone();
- wire.pubkey = "11".repeat(32);
- assert_mismatch(raw_event_from_wire(wire), &signed_event);
-
- let mut wire = signed_event.wire().clone();
- wire.created_at += 1;
- assert_mismatch(raw_event_from_wire(wire), &signed_event);
-
- let mut wire = signed_event.wire().clone();
- wire.kind += 1;
- assert_mismatch(raw_event_from_wire(wire), &signed_event);
-
- let mut wire = signed_event.wire().clone();
- wire.content.push_str(" changed");
- assert_mismatch(raw_event_from_wire(wire), &signed_event);
-
- let mut wire = signed_event.wire().clone();
- wire.sig = "22".repeat(64);
- assert_mismatch(raw_event_from_wire(wire), &signed_event);
-
- let mut wire = signed_event.wire().clone();
- wire.tags.push(vec!["t".to_owned(), "compost".to_owned()]);
- assert_mismatch(raw_event_from_wire(wire), &signed_event);
- }
-}
diff --git a/crates/transport_nostr/src/relays.rs b/crates/transport_nostr/src/relays.rs
@@ -1,21 +0,0 @@
-use crate::{RadrootsNostrClient, RadrootsRelayTransportError};
-
-pub async fn radroots_nostr_add_relay(
- client: &RadrootsNostrClient,
- url: &str,
-) -> Result<(), RadrootsRelayTransportError> {
- client.add_relay(url).await?;
- Ok(())
-}
-
-pub async fn radroots_nostr_remove_relay(
- client: &RadrootsNostrClient,
- url: &str,
-) -> Result<(), RadrootsRelayTransportError> {
- client.remove_relay(url).await?;
- Ok(())
-}
-
-pub async fn radroots_nostr_connect(client: &RadrootsNostrClient) {
- client.connect().await;
-}
diff --git a/crates/transport_nostr/tests/package_boundary.rs b/crates/transport_nostr/tests/package_boundary.rs
@@ -1,4 +1,6 @@
use std::collections::BTreeSet;
+use std::fs;
+use std::path::Path;
const MANIFEST: &str = include_str!("../Cargo.toml");
const ROOT: &str = include_str!("../src/lib.rs");
@@ -67,3 +69,44 @@ fn private_modules(root: &str) -> BTreeSet<&str> {
.filter_map(|module| module.strip_suffix(';'))
.collect()
}
+
+#[test]
+fn adapter_owns_no_storage_outbox_or_orchestration_surface() {
+ for forbidden in [
+ "radroots_event_store",
+ "radroots_outbox",
+ "radroots_storage",
+ "publish_claimed",
+ "fetch_and_ingest",
+ "projection_refresh",
+ "retry_schedule",
+ ] {
+ assert!(!MANIFEST.contains(forbidden));
+ assert!(!ROOT.contains(forbidden));
+ }
+
+ let source_root = Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
+ let source_files = fs::read_dir(source_root)
+ .expect("source directory")
+ .map(|entry| {
+ entry
+ .expect("source entry")
+ .file_name()
+ .into_string()
+ .expect("utf-8 source name")
+ })
+ .collect::<BTreeSet<_>>();
+ assert_eq!(
+ source_files,
+ BTreeSet::from([
+ "auth.rs".to_owned(),
+ "client.rs".to_owned(),
+ "error.rs".to_owned(),
+ "lib.rs".to_owned(),
+ "relay.rs".to_owned(),
+ "sink.rs".to_owned(),
+ "source.rs".to_owned(),
+ "status.rs".to_owned(),
+ ])
+ );
+}
diff --git a/tools/xtask/src/contract.rs b/tools/xtask/src/contract.rs
@@ -2597,16 +2597,10 @@ const TRADE_VALIDATION_RECEIPT_WITNESSES: [EventBoundarySourceWitness; 2] = [
},
];
-const RELAY_DOC_WITNESSES: [EventBoundarySourceWitness; 2] = [
- EventBoundarySourceWitness {
- relative_path: "crates/event/src/relay_document.rs",
- required_fragments: &["pub struct RelayDocument"],
- },
- EventBoundarySourceWitness {
- relative_path: "crates/transport_nostr/src/nip11.rs",
- required_fragments: &["pub async fn fetch_nip11(ws_url: &str) -> Option<RelayDocument>"],
- },
-];
+const RELAY_DOC_WITNESSES: [EventBoundarySourceWitness; 1] = [EventBoundarySourceWitness {
+ relative_path: "crates/event/src/relay_document.rs",
+ required_fragments: &["pub struct RelayDocument"],
+}];
const CANONICAL_EVENT_BOUNDARY_EXPECTATIONS: [EventBoundaryExpectation; 44] = [
EventBoundaryExpectation {