commit 1c2e5bbe39282ebfb73d885f224e412248165589
parent 9ef5c5a3e1a12fb6e3989aa53c669608b9c2fd9f
Author: triesap <tyson@radroots.org>
Date: Sun, 19 Jul 2026 02:54:31 +0000
tests: execute operational listing conformance
- replace symbolic codec vectors with exact Victoria listing wire cases
- verify classified addresses and signed NIP-01 trade projections
- govern packaged mirrors and operation case authority
- reject fixture drift in schema, geography, signatures, and errors
Diffstat:
14 files changed, 2381 insertions(+), 54 deletions(-)
diff --git a/contracts/conformance/vectors/events/operational_listing_tags_full.v1.json b/contracts/conformance/vectors/events/operational_listing_tags_full.v1.json
@@ -3,24 +3,170 @@
"contract_version": "1.0.0",
"vectors": [
{
- "id": "operational_listing_tags_full_minimal_001",
+ "id": "operational_listing_tags_full_victoria_carrots_001",
"kind": "operational_listing_tags_full",
"input": {
- "d_tag": "listing-001",
- "farm": {
- "pubkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
- "d_tag": "farm-001"
- },
- "product": {
- "key": "coffee.green",
- "title": "green coffee lot",
- "category": "coffee"
- },
- "primary_bin_id": "bin-a",
- "bins": []
+ "listing": {
+ "d_tag": "AAAAAAAAAAAAAAAAAAAAAg",
+ "published_at": 1700000000,
+ "farm": {
+ "pubkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
+ "d_tag": "AAAAAAAAAAAAAAAAAAAAAA"
+ },
+ "product": {
+ "key": "carrot-nantes",
+ "title": "Nantes Carrots",
+ "category": "produce",
+ "summary": "Fresh bunches harvested in Saanich"
+ },
+ "primary_bin_id": "bunch",
+ "bins": [
+ {
+ "bin_id": "bunch",
+ "quantity": {
+ "amount": "1",
+ "unit": "each"
+ },
+ "price_per_canonical_unit": {
+ "amount": {
+ "amount": "4",
+ "currency": "CAD"
+ },
+ "quantity": {
+ "amount": "1",
+ "unit": "each"
+ }
+ }
+ }
+ ],
+ "inventory_available": "24",
+ "availability": {
+ "kind": "status",
+ "amount": {
+ "status": {
+ "kind": "active"
+ }
+ }
+ },
+ "delivery_method": {
+ "kind": "pickup"
+ },
+ "location": {
+ "primary": "Saanich Peninsula",
+ "city": "Victoria",
+ "region": "BC",
+ "country": "CA",
+ "geohash": "c28hr"
+ }
+ }
},
"expected": {
- "tags": []
+ "tags": [
+ [
+ "d",
+ "AAAAAAAAAAAAAAAAAAAAAg"
+ ],
+ [
+ "p",
+ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
+ ],
+ [
+ "a",
+ "30340:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:AAAAAAAAAAAAAAAAAAAAAA"
+ ],
+ [
+ "key",
+ "carrot-nantes"
+ ],
+ [
+ "title",
+ "Nantes Carrots"
+ ],
+ [
+ "category",
+ "produce"
+ ],
+ [
+ "summary",
+ "Fresh bunches harvested in Saanich"
+ ],
+ [
+ "published_at",
+ "1700000000"
+ ],
+ [
+ "radroots:primary_bin",
+ "bunch"
+ ],
+ [
+ "radroots:bin",
+ "bunch",
+ "1",
+ "each"
+ ],
+ [
+ "radroots:price",
+ "bunch",
+ "4",
+ "CAD",
+ "1",
+ "each"
+ ],
+ [
+ "price",
+ "4",
+ "CAD"
+ ],
+ [
+ "inventory",
+ "24"
+ ],
+ [
+ "status",
+ "active"
+ ],
+ [
+ "delivery",
+ "pickup"
+ ],
+ [
+ "location",
+ "Saanich Peninsula",
+ "Victoria",
+ "BC",
+ "CA"
+ ],
+ [
+ "g",
+ "c28hr"
+ ]
+ ]
+ }
+ },
+ {
+ "id": "operational_listing_tags_full_empty_bins_002",
+ "kind": "operational_listing_tags_full",
+ "input": {
+ "listing": {
+ "d_tag": "AAAAAAAAAAAAAAAAAAAAAw",
+ "farm": {
+ "pubkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
+ "d_tag": "AAAAAAAAAAAAAAAAAAAAAA"
+ },
+ "product": {
+ "key": "carrot-nantes",
+ "title": "Nantes Carrots",
+ "category": "produce"
+ },
+ "primary_bin_id": "bunch",
+ "bins": []
+ }
+ },
+ "expected": {
+ "error": {
+ "code": "empty_required_field",
+ "field": "bins"
+ }
}
}
]
diff --git a/contracts/conformance/vectors/operational_listing/build_draft.v1.json b/contracts/conformance/vectors/operational_listing/build_draft.v1.json
@@ -3,29 +3,173 @@
"contract_version": "1.0.0",
"vectors": [
{
- "id": "operational_listing_build_draft_minimal_001",
+ "id": "operational_listing_build_draft_victoria_carrots_001",
"kind": "operational_listing.build_draft",
"input": {
"listing": {
"d_tag": "AAAAAAAAAAAAAAAAAAAAAg",
+ "published_at": 1700000000,
"farm": {
- "pubkey": "farm_pubkey",
+ "pubkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"d_tag": "AAAAAAAAAAAAAAAAAAAAAA"
},
"product": {
- "key": "sku",
- "title": "widget",
- "category": "tools"
+ "key": "carrot-nantes",
+ "title": "Nantes Carrots",
+ "category": "produce",
+ "summary": "Fresh bunches harvested in Saanich"
},
- "primary_bin_id": "bin-1",
- "bins": []
+ "primary_bin_id": "bunch",
+ "bins": [
+ {
+ "bin_id": "bunch",
+ "quantity": {
+ "amount": "1",
+ "unit": "each"
+ },
+ "price_per_canonical_unit": {
+ "amount": {
+ "amount": "4",
+ "currency": "CAD"
+ },
+ "quantity": {
+ "amount": "1",
+ "unit": "each"
+ }
+ }
+ }
+ ],
+ "inventory_available": "24",
+ "availability": {
+ "kind": "status",
+ "amount": {
+ "status": {
+ "kind": "active"
+ }
+ }
+ },
+ "delivery_method": {
+ "kind": "pickup"
+ },
+ "location": {
+ "primary": "Saanich Peninsula",
+ "city": "Victoria",
+ "region": "BC",
+ "country": "CA",
+ "geohash": "c28hr"
+ }
}
},
"expected": {
"wire_parts": {
- "kind": "classified_listing",
- "content_shape": "operational_listing_markdown_or_empty",
- "tags_shape": "operational_listing_tags_full"
+ "kind": 30402,
+ "content": "# Nantes Carrots\n\nFresh bunches harvested in Saanich",
+ "tags": [
+ [
+ "d",
+ "AAAAAAAAAAAAAAAAAAAAAg"
+ ],
+ [
+ "p",
+ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
+ ],
+ [
+ "a",
+ "30340:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:AAAAAAAAAAAAAAAAAAAAAA"
+ ],
+ [
+ "key",
+ "carrot-nantes"
+ ],
+ [
+ "title",
+ "Nantes Carrots"
+ ],
+ [
+ "category",
+ "produce"
+ ],
+ [
+ "summary",
+ "Fresh bunches harvested in Saanich"
+ ],
+ [
+ "published_at",
+ "1700000000"
+ ],
+ [
+ "radroots:primary_bin",
+ "bunch"
+ ],
+ [
+ "radroots:bin",
+ "bunch",
+ "1",
+ "each"
+ ],
+ [
+ "radroots:price",
+ "bunch",
+ "4",
+ "CAD",
+ "1",
+ "each"
+ ],
+ [
+ "price",
+ "4",
+ "CAD"
+ ],
+ [
+ "inventory",
+ "24"
+ ],
+ [
+ "status",
+ "active"
+ ],
+ [
+ "delivery",
+ "pickup"
+ ],
+ [
+ "location",
+ "Saanich Peninsula",
+ "Victoria",
+ "BC",
+ "CA"
+ ],
+ [
+ "g",
+ "c28hr"
+ ]
+ ]
+ }
+ }
+ },
+ {
+ "id": "operational_listing_build_draft_empty_bins_002",
+ "kind": "operational_listing.build_draft",
+ "input": {
+ "listing": {
+ "d_tag": "AAAAAAAAAAAAAAAAAAAAAw",
+ "farm": {
+ "pubkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
+ "d_tag": "AAAAAAAAAAAAAAAAAAAAAA"
+ },
+ "product": {
+ "key": "carrot-nantes",
+ "title": "Nantes Carrots",
+ "category": "produce"
+ },
+ "primary_bin_id": "bunch",
+ "bins": []
+ }
+ },
+ "expected": {
+ "error": {
+ "code": "empty_required_field",
+ "field": "bins"
}
}
}
diff --git a/contracts/conformance/vectors/operational_listing/build_tags.v1.json b/contracts/conformance/vectors/operational_listing/build_tags.v1.json
@@ -3,26 +3,158 @@
"contract_version": "1.0.0",
"vectors": [
{
- "id": "operational_listing_build_tags_minimal_001",
+ "id": "operational_listing_build_tags_victoria_carrots_001",
"kind": "operational_listing.build_tags",
"input": {
"listing": {
"d_tag": "AAAAAAAAAAAAAAAAAAAAAg",
+ "published_at": 1700000000,
"farm": {
- "pubkey": "farm_pubkey",
+ "pubkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"d_tag": "AAAAAAAAAAAAAAAAAAAAAA"
},
"product": {
- "key": "sku",
- "title": "widget",
- "category": "tools"
+ "key": "carrot-nantes",
+ "title": "Nantes Carrots",
+ "category": "produce",
+ "summary": "Fresh bunches harvested in Saanich"
},
- "primary_bin_id": "bin-1",
+ "primary_bin_id": "bunch",
+ "bins": [
+ {
+ "bin_id": "bunch",
+ "quantity": {
+ "amount": "1",
+ "unit": "each"
+ },
+ "price_per_canonical_unit": {
+ "amount": {
+ "amount": "4",
+ "currency": "CAD"
+ },
+ "quantity": {
+ "amount": "1",
+ "unit": "each"
+ }
+ }
+ }
+ ],
+ "inventory_available": "24",
+ "availability": {
+ "kind": "status",
+ "amount": {
+ "status": {
+ "kind": "active"
+ }
+ }
+ },
+ "delivery_method": {
+ "kind": "pickup"
+ },
+ "location": {
+ "primary": "Saanich Peninsula",
+ "city": "Victoria",
+ "region": "BC",
+ "country": "CA",
+ "geohash": "c28hr"
+ }
+ }
+ },
+ "expected": {
+ "tags": [
+ [
+ "d",
+ "AAAAAAAAAAAAAAAAAAAAAg"
+ ],
+ [
+ "p",
+ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
+ ],
+ [
+ "a",
+ "30340:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:AAAAAAAAAAAAAAAAAAAAAA"
+ ],
+ [
+ "key",
+ "carrot-nantes"
+ ],
+ [
+ "title",
+ "Nantes Carrots"
+ ],
+ [
+ "category",
+ "produce"
+ ],
+ [
+ "summary",
+ "Fresh bunches harvested in Saanich"
+ ],
+ [
+ "published_at",
+ "1700000000"
+ ],
+ [
+ "radroots:primary_bin",
+ "bunch"
+ ],
+ [
+ "radroots:bin",
+ "bunch",
+ "1",
+ "each"
+ ],
+ [
+ "radroots:price",
+ "bunch",
+ "4",
+ "CAD",
+ "1",
+ "each"
+ ],
+ [
+ "price",
+ "4",
+ "CAD"
+ ],
+ [
+ "location",
+ "Saanich Peninsula",
+ "Victoria",
+ "BC",
+ "CA"
+ ],
+ [
+ "g",
+ "c28hr"
+ ]
+ ]
+ }
+ },
+ {
+ "id": "operational_listing_build_tags_empty_bins_002",
+ "kind": "operational_listing.build_tags",
+ "input": {
+ "listing": {
+ "d_tag": "AAAAAAAAAAAAAAAAAAAAAw",
+ "farm": {
+ "pubkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
+ "d_tag": "AAAAAAAAAAAAAAAAAAAAAA"
+ },
+ "product": {
+ "key": "carrot-nantes",
+ "title": "Nantes Carrots",
+ "category": "produce"
+ },
+ "primary_bin_id": "bunch",
"bins": []
}
},
"expected": {
- "tags_shape": "operational_listing_tags"
+ "error": {
+ "code": "empty_required_field",
+ "field": "bins"
+ }
}
}
]
diff --git a/contracts/conformance/vectors/trade/parse_classified_listing_address.v1.json b/contracts/conformance/vectors/trade/parse_classified_listing_address.v1.json
@@ -3,13 +3,98 @@
"contract_version": "1.0.0",
"vectors": [
{
- "id": "trade_parse_classified_listing_address_minimal_001",
- "kind": "trade.parse_classified_listing_address",
+ "id": "trade_parse_classified_listing_address_canonical_001",
+ "kind": "trade.parse_classified_listing_address.valid",
"input": {
"listing_addr": "30402:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:AAAAAAAAAAAAAAAAAAAAAg"
},
"expected": {
- "address_shape": "classified_listing_address"
+ "address": "30402:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:AAAAAAAAAAAAAAAAAAAAAg",
+ "kind": 30402,
+ "seller_pubkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
+ "listing_id": "AAAAAAAAAAAAAAAAAAAAAg"
+ }
+ },
+ {
+ "id": "trade_parse_classified_listing_address_normalized_002",
+ "kind": "trade.parse_classified_listing_address.valid",
+ "input": {
+ "listing_addr": "+030402:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA:AAAAAAAAAAAAAAAAAAAAAg"
+ },
+ "expected": {
+ "address": "30402:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:AAAAAAAAAAAAAAAAAAAAAg",
+ "kind": 30402,
+ "seller_pubkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
+ "listing_id": "AAAAAAAAAAAAAAAAAAAAAg"
+ }
+ },
+ {
+ "id": "trade_parse_classified_listing_address_wrong_kind_003",
+ "kind": "trade.parse_classified_listing_address.invalid",
+ "input": {
+ "listing_addr": "30403:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:AAAAAAAAAAAAAAAAAAAAAg"
+ },
+ "expected": {
+ "error": {
+ "kind": "unexpected_kind",
+ "expected": 30402,
+ "actual": 30403
+ },
+ "message": "identifier kind 30403 does not match required kind 30402"
+ }
+ },
+ {
+ "id": "trade_parse_classified_listing_address_malformed_format_004",
+ "kind": "trade.parse_classified_listing_address.invalid",
+ "input": {
+ "listing_addr": "30402"
+ },
+ "expected": {
+ "error": {
+ "kind": "invalid_format"
+ },
+ "message": "identifier has invalid format"
+ }
+ },
+ {
+ "id": "trade_parse_classified_listing_address_short_pubkey_005",
+ "kind": "trade.parse_classified_listing_address.invalid",
+ "input": {
+ "listing_addr": "30402:not_hex:AAAAAAAAAAAAAAAAAAAAAg"
+ },
+ "expected": {
+ "error": {
+ "kind": "invalid_length",
+ "expected": 64,
+ "actual": 7
+ },
+ "message": "identifier length 7 does not match required length 64"
+ }
+ },
+ {
+ "id": "trade_parse_classified_listing_address_non_hex_pubkey_006",
+ "kind": "trade.parse_classified_listing_address.invalid",
+ "input": {
+ "listing_addr": "30402:gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg:AAAAAAAAAAAAAAAAAAAAAg"
+ },
+ "expected": {
+ "error": {
+ "kind": "invalid_character"
+ },
+ "message": "identifier contains an invalid character"
+ }
+ },
+ {
+ "id": "trade_parse_classified_listing_address_invalid_d_tag_007",
+ "kind": "trade.parse_classified_listing_address.invalid",
+ "input": {
+ "listing_addr": "30402:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:bad d"
+ },
+ "expected": {
+ "error": {
+ "kind": "invalid_character"
+ },
+ "message": "identifier contains an invalid character"
}
}
]
diff --git a/contracts/conformance/vectors/trade_validation/validate_operational_listing_event.v1.json b/contracts/conformance/vectors/trade_validation/validate_operational_listing_event.v1.json
@@ -3,17 +3,219 @@
"contract_version": "1.0.0",
"vectors": [
{
- "id": "trade_validation_validate_operational_listing_event_minimal_001",
- "kind": "trade_validation.validate_operational_listing_event",
+ "id": "trade_validation_validate_operational_listing_event_valid_001",
+ "kind": "trade_validation.validate_operational_listing_event.valid",
"input": {
"event": {
- "kind": "classified_listing",
- "content_shape": "operational_listing_json_or_markdown",
- "tags_shape": "operational_listing_tags_full"
+ "id": "6739ed38175521d1b8a64592ec3407332ee24449e1e7353fd8f721c0995b9d8f",
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "created_at": 1700000000,
+ "kind": 30402,
+ "tags": [
+ ["d", "AAAAAAAAAAAAAAAAAAAAAg"],
+ ["p", "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df"],
+ ["a", "30340:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:AAAAAAAAAAAAAAAAAAAAAA"],
+ ["key", "carrot-nantes"],
+ ["title", "Nantes Carrots"],
+ ["category", "produce"],
+ ["summary", "Fresh bunches harvested in Saanich"],
+ ["published_at", "1700000000"],
+ ["radroots:primary_bin", "bunch"],
+ ["radroots:bin", "bunch", "1", "each"],
+ ["radroots:price", "bunch", "4", "CAD", "1", "each"],
+ ["price", "4", "CAD"],
+ ["inventory", "24"],
+ ["status", "active"],
+ ["delivery", "pickup"],
+ ["location", "Saanich Peninsula", "Victoria", "BC", "CA"],
+ ["g", "c28hr"]
+ ],
+ "content": "# Nantes Carrots\n\nFresh bunches harvested in Saanich",
+ "sig": "ef3413c4ac4be3f748fcb51b3e5b9b03c590f5f814dbd6ab3f2f2c26dbc87eb1347cefbe97abacce60f0c4530848695e766d3a950350c72089813b3318a3f944"
}
},
"expected": {
- "validation_shape": "operational_listing_trade_projection"
+ "projection": {
+ "listing_id": "AAAAAAAAAAAAAAAAAAAAAg",
+ "listing_addr": "30402:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:AAAAAAAAAAAAAAAAAAAAAg",
+ "seller_pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "title": "Nantes Carrots",
+ "description": "Fresh bunches harvested in Saanich",
+ "product_type": "produce",
+ "primary_bin_id": "bunch",
+ "bin_quantity": {
+ "amount": "1",
+ "unit": "each"
+ },
+ "unit": "each",
+ "unit_price": {
+ "amount": "4",
+ "currency": "CAD"
+ },
+ "inventory_available": "24",
+ "availability": {
+ "kind": "status",
+ "amount": {
+ "status": {
+ "kind": "active"
+ }
+ }
+ },
+ "location": {
+ "primary": "Saanich Peninsula",
+ "city": "Victoria",
+ "region": "BC",
+ "country": "CA",
+ "geohash": "c28hr"
+ },
+ "delivery_method": {
+ "kind": "pickup"
+ },
+ "listing": {
+ "d_tag": "AAAAAAAAAAAAAAAAAAAAAg",
+ "published_at": 1700000000,
+ "farm": {
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "d_tag": "AAAAAAAAAAAAAAAAAAAAAA"
+ },
+ "product": {
+ "key": "carrot-nantes",
+ "title": "Nantes Carrots",
+ "category": "produce",
+ "summary": "Fresh bunches harvested in Saanich",
+ "process": null,
+ "lot": null,
+ "location": null,
+ "profile": null,
+ "year": null
+ },
+ "primary_bin_id": "bunch",
+ "bins": [
+ {
+ "bin_id": "bunch",
+ "quantity": {
+ "amount": "1",
+ "unit": "each"
+ },
+ "price_per_canonical_unit": {
+ "amount": {
+ "amount": "4",
+ "currency": "CAD"
+ },
+ "quantity": {
+ "amount": "1",
+ "unit": "each"
+ }
+ },
+ "display_amount": null,
+ "display_unit": null,
+ "display_label": null,
+ "display_price": null,
+ "display_price_unit": null
+ }
+ ],
+ "resource_area": null,
+ "plot": null,
+ "discounts": null,
+ "inventory_available": "24",
+ "availability": {
+ "kind": "status",
+ "amount": {
+ "status": {
+ "kind": "active"
+ }
+ }
+ },
+ "delivery_method": {
+ "kind": "pickup"
+ },
+ "location": {
+ "primary": "Saanich Peninsula",
+ "city": "Victoria",
+ "region": "BC",
+ "country": "CA",
+ "geohash": "c28hr"
+ },
+ "images": null
+ }
+ }
+ }
+ },
+ {
+ "id": "trade_validation_validate_operational_listing_event_invalid_seller_002",
+ "kind": "trade_validation.validate_operational_listing_event.invalid",
+ "input": {
+ "event": {
+ "id": "294177fd3f698947e8f1be9f1f3429c87d87d17a643b1c3666674de7bbe6eb20",
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "created_at": 1700000000,
+ "kind": 30402,
+ "tags": [
+ ["d", "AAAAAAAAAAAAAAAAAAAAAg"],
+ ["p", "e0266e3cfb0d2886f91c73f5f868f3b98273713e5fcd97c081663f5518a4b3af"],
+ ["a", "30340:e0266e3cfb0d2886f91c73f5f868f3b98273713e5fcd97c081663f5518a4b3af:AAAAAAAAAAAAAAAAAAAAAA"],
+ ["key", "carrot-nantes"],
+ ["title", "Nantes Carrots"],
+ ["category", "produce"],
+ ["summary", "Fresh bunches harvested in Saanich"],
+ ["published_at", "1700000000"],
+ ["radroots:primary_bin", "bunch"],
+ ["radroots:bin", "bunch", "1", "each"],
+ ["radroots:price", "bunch", "4", "CAD", "1", "each"],
+ ["price", "4", "CAD"],
+ ["inventory", "24"],
+ ["status", "active"],
+ ["delivery", "pickup"],
+ ["location", "Saanich Peninsula", "Victoria", "BC", "CA"],
+ ["g", "c28hr"]
+ ],
+ "content": "# Nantes Carrots\n\nFresh bunches harvested in Saanich",
+ "sig": "0b12724932817061d949d1d4543beaa90b27b08f09f63422871dda01ee692b26c4ff9ef66b31ea4500b76d1c8e45c2126c96b7dc17b8f242b29b97a5a7294b73"
+ }
+ },
+ "expected": {
+ "error": {
+ "kind": "invalid_seller"
+ },
+ "message": "listing author does not match farm pubkey"
+ }
+ },
+ {
+ "id": "trade_validation_validate_operational_listing_event_missing_inventory_003",
+ "kind": "trade_validation.validate_operational_listing_event.invalid",
+ "input": {
+ "event": {
+ "id": "b2d934d5b9a089a500c7c2a76fb1486b4abf2dd4b9334e3dc00114884818e77a",
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "created_at": 1700000000,
+ "kind": 30402,
+ "tags": [
+ ["d", "AAAAAAAAAAAAAAAAAAAAAg"],
+ ["p", "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df"],
+ ["a", "30340:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:AAAAAAAAAAAAAAAAAAAAAA"],
+ ["key", "carrot-nantes"],
+ ["title", "Nantes Carrots"],
+ ["category", "produce"],
+ ["summary", "Fresh bunches harvested in Saanich"],
+ ["published_at", "1700000000"],
+ ["radroots:primary_bin", "bunch"],
+ ["radroots:bin", "bunch", "1", "each"],
+ ["radroots:price", "bunch", "4", "CAD", "1", "each"],
+ ["price", "4", "CAD"],
+ ["status", "active"],
+ ["delivery", "pickup"],
+ ["location", "Saanich Peninsula", "Victoria", "BC", "CA"],
+ ["g", "c28hr"]
+ ],
+ "content": "# Nantes Carrots\n\nFresh bunches harvested in Saanich",
+ "sig": "3504a764b97f8fd2915281e8c110247979da5f62743967227711a08c08de5a87ad79500def9da29730e12175dbe656f75b75fe2dae4a4e25c7f989aecd3520ef"
+ }
+ },
+ "expected": {
+ "error": {
+ "kind": "missing_inventory"
+ },
+ "message": "missing listing inventory"
}
}
]
diff --git a/contracts/operations.toml b/contracts/operations.toml
@@ -1216,18 +1216,29 @@ domain = "trade"
id = "trade.parse_classified_listing_address"
stability = "beta"
inputs = ["listing_addr"]
-outputs = ["RadrootsClassifiedListingAddress"]
+outputs = ["RadrootsClassifiedListingAddressParts"]
error_class = "address_error"
deterministic = true
signing = "native"
transport = "native"
[operations.trade_parse_classified_listing_address.implementation]
-rust_modules = ["crates/event/src/ids.rs"]
-rust_types = ["radroots_event::ids::RadrootsClassifiedListingAddress"]
+rust_modules = [
+ "crates/event/src/ids.rs",
+ "crates/trade/src/operational_listing/mod.rs",
+]
+rust_types = [
+ "radroots_event::ids::RadrootsClassifiedListingAddress",
+ "radroots_event::ids::RadrootsIdParseError",
+ "radroots_trade::operational_listing::RadrootsClassifiedListingAddressParts",
+]
[operations.trade_parse_classified_listing_address.conformance]
vector = "contracts/conformance/vectors/trade/parse_classified_listing_address.v1.json"
+case_kinds = [
+ "trade.parse_classified_listing_address.valid",
+ "trade.parse_classified_listing_address.invalid",
+]
[operations.trade_reduce_records]
domain = "trade"
@@ -1262,14 +1273,22 @@ signing = "native"
transport = "native"
[operations.trade_validation_validate_operational_listing_event.implementation]
-rust_modules = ["crates/trade/src/operational_listing/validation.rs"]
+rust_modules = [
+ "crates/event/src/trade_validation.rs",
+ "crates/trade/src/operational_listing/validation.rs",
+]
rust_types = [
"radroots_event::RadrootsEventEnvelope",
+ "radroots_event::trade_validation::RadrootsOperationalListingValidationError",
"radroots_trade::operational_listing::validation::RadrootsOperationalListingTradeProjection",
]
[operations.trade_validation_validate_operational_listing_event.conformance]
vector = "contracts/conformance/vectors/trade_validation/validate_operational_listing_event.v1.json"
+case_kinds = [
+ "trade_validation.validate_operational_listing_event.valid",
+ "trade_validation.validate_operational_listing_event.invalid",
+]
[operations.knowledge_contract_manifest_json]
domain = "knowledge"
diff --git a/crates/event_codec/tests/fixtures/operational_listing_build_draft.v1.json b/crates/event_codec/tests/fixtures/operational_listing_build_draft.v1.json
@@ -0,0 +1,177 @@
+{
+ "suite": "operational_listing",
+ "contract_version": "1.0.0",
+ "vectors": [
+ {
+ "id": "operational_listing_build_draft_victoria_carrots_001",
+ "kind": "operational_listing.build_draft",
+ "input": {
+ "listing": {
+ "d_tag": "AAAAAAAAAAAAAAAAAAAAAg",
+ "published_at": 1700000000,
+ "farm": {
+ "pubkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
+ "d_tag": "AAAAAAAAAAAAAAAAAAAAAA"
+ },
+ "product": {
+ "key": "carrot-nantes",
+ "title": "Nantes Carrots",
+ "category": "produce",
+ "summary": "Fresh bunches harvested in Saanich"
+ },
+ "primary_bin_id": "bunch",
+ "bins": [
+ {
+ "bin_id": "bunch",
+ "quantity": {
+ "amount": "1",
+ "unit": "each"
+ },
+ "price_per_canonical_unit": {
+ "amount": {
+ "amount": "4",
+ "currency": "CAD"
+ },
+ "quantity": {
+ "amount": "1",
+ "unit": "each"
+ }
+ }
+ }
+ ],
+ "inventory_available": "24",
+ "availability": {
+ "kind": "status",
+ "amount": {
+ "status": {
+ "kind": "active"
+ }
+ }
+ },
+ "delivery_method": {
+ "kind": "pickup"
+ },
+ "location": {
+ "primary": "Saanich Peninsula",
+ "city": "Victoria",
+ "region": "BC",
+ "country": "CA",
+ "geohash": "c28hr"
+ }
+ }
+ },
+ "expected": {
+ "wire_parts": {
+ "kind": 30402,
+ "content": "# Nantes Carrots\n\nFresh bunches harvested in Saanich",
+ "tags": [
+ [
+ "d",
+ "AAAAAAAAAAAAAAAAAAAAAg"
+ ],
+ [
+ "p",
+ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
+ ],
+ [
+ "a",
+ "30340:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:AAAAAAAAAAAAAAAAAAAAAA"
+ ],
+ [
+ "key",
+ "carrot-nantes"
+ ],
+ [
+ "title",
+ "Nantes Carrots"
+ ],
+ [
+ "category",
+ "produce"
+ ],
+ [
+ "summary",
+ "Fresh bunches harvested in Saanich"
+ ],
+ [
+ "published_at",
+ "1700000000"
+ ],
+ [
+ "radroots:primary_bin",
+ "bunch"
+ ],
+ [
+ "radroots:bin",
+ "bunch",
+ "1",
+ "each"
+ ],
+ [
+ "radroots:price",
+ "bunch",
+ "4",
+ "CAD",
+ "1",
+ "each"
+ ],
+ [
+ "price",
+ "4",
+ "CAD"
+ ],
+ [
+ "inventory",
+ "24"
+ ],
+ [
+ "status",
+ "active"
+ ],
+ [
+ "delivery",
+ "pickup"
+ ],
+ [
+ "location",
+ "Saanich Peninsula",
+ "Victoria",
+ "BC",
+ "CA"
+ ],
+ [
+ "g",
+ "c28hr"
+ ]
+ ]
+ }
+ }
+ },
+ {
+ "id": "operational_listing_build_draft_empty_bins_002",
+ "kind": "operational_listing.build_draft",
+ "input": {
+ "listing": {
+ "d_tag": "AAAAAAAAAAAAAAAAAAAAAw",
+ "farm": {
+ "pubkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
+ "d_tag": "AAAAAAAAAAAAAAAAAAAAAA"
+ },
+ "product": {
+ "key": "carrot-nantes",
+ "title": "Nantes Carrots",
+ "category": "produce"
+ },
+ "primary_bin_id": "bunch",
+ "bins": []
+ }
+ },
+ "expected": {
+ "error": {
+ "code": "empty_required_field",
+ "field": "bins"
+ }
+ }
+ }
+ ]
+}
diff --git a/crates/event_codec/tests/fixtures/operational_listing_build_tags.v1.json b/crates/event_codec/tests/fixtures/operational_listing_build_tags.v1.json
@@ -0,0 +1,161 @@
+{
+ "suite": "operational_listing",
+ "contract_version": "1.0.0",
+ "vectors": [
+ {
+ "id": "operational_listing_build_tags_victoria_carrots_001",
+ "kind": "operational_listing.build_tags",
+ "input": {
+ "listing": {
+ "d_tag": "AAAAAAAAAAAAAAAAAAAAAg",
+ "published_at": 1700000000,
+ "farm": {
+ "pubkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
+ "d_tag": "AAAAAAAAAAAAAAAAAAAAAA"
+ },
+ "product": {
+ "key": "carrot-nantes",
+ "title": "Nantes Carrots",
+ "category": "produce",
+ "summary": "Fresh bunches harvested in Saanich"
+ },
+ "primary_bin_id": "bunch",
+ "bins": [
+ {
+ "bin_id": "bunch",
+ "quantity": {
+ "amount": "1",
+ "unit": "each"
+ },
+ "price_per_canonical_unit": {
+ "amount": {
+ "amount": "4",
+ "currency": "CAD"
+ },
+ "quantity": {
+ "amount": "1",
+ "unit": "each"
+ }
+ }
+ }
+ ],
+ "inventory_available": "24",
+ "availability": {
+ "kind": "status",
+ "amount": {
+ "status": {
+ "kind": "active"
+ }
+ }
+ },
+ "delivery_method": {
+ "kind": "pickup"
+ },
+ "location": {
+ "primary": "Saanich Peninsula",
+ "city": "Victoria",
+ "region": "BC",
+ "country": "CA",
+ "geohash": "c28hr"
+ }
+ }
+ },
+ "expected": {
+ "tags": [
+ [
+ "d",
+ "AAAAAAAAAAAAAAAAAAAAAg"
+ ],
+ [
+ "p",
+ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
+ ],
+ [
+ "a",
+ "30340:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:AAAAAAAAAAAAAAAAAAAAAA"
+ ],
+ [
+ "key",
+ "carrot-nantes"
+ ],
+ [
+ "title",
+ "Nantes Carrots"
+ ],
+ [
+ "category",
+ "produce"
+ ],
+ [
+ "summary",
+ "Fresh bunches harvested in Saanich"
+ ],
+ [
+ "published_at",
+ "1700000000"
+ ],
+ [
+ "radroots:primary_bin",
+ "bunch"
+ ],
+ [
+ "radroots:bin",
+ "bunch",
+ "1",
+ "each"
+ ],
+ [
+ "radroots:price",
+ "bunch",
+ "4",
+ "CAD",
+ "1",
+ "each"
+ ],
+ [
+ "price",
+ "4",
+ "CAD"
+ ],
+ [
+ "location",
+ "Saanich Peninsula",
+ "Victoria",
+ "BC",
+ "CA"
+ ],
+ [
+ "g",
+ "c28hr"
+ ]
+ ]
+ }
+ },
+ {
+ "id": "operational_listing_build_tags_empty_bins_002",
+ "kind": "operational_listing.build_tags",
+ "input": {
+ "listing": {
+ "d_tag": "AAAAAAAAAAAAAAAAAAAAAw",
+ "farm": {
+ "pubkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
+ "d_tag": "AAAAAAAAAAAAAAAAAAAAAA"
+ },
+ "product": {
+ "key": "carrot-nantes",
+ "title": "Nantes Carrots",
+ "category": "produce"
+ },
+ "primary_bin_id": "bunch",
+ "bins": []
+ }
+ },
+ "expected": {
+ "error": {
+ "code": "empty_required_field",
+ "field": "bins"
+ }
+ }
+ }
+ ]
+}
diff --git a/crates/event_codec/tests/fixtures/operational_listing_tags_full.v1.json b/crates/event_codec/tests/fixtures/operational_listing_tags_full.v1.json
@@ -0,0 +1,173 @@
+{
+ "suite": "events",
+ "contract_version": "1.0.0",
+ "vectors": [
+ {
+ "id": "operational_listing_tags_full_victoria_carrots_001",
+ "kind": "operational_listing_tags_full",
+ "input": {
+ "listing": {
+ "d_tag": "AAAAAAAAAAAAAAAAAAAAAg",
+ "published_at": 1700000000,
+ "farm": {
+ "pubkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
+ "d_tag": "AAAAAAAAAAAAAAAAAAAAAA"
+ },
+ "product": {
+ "key": "carrot-nantes",
+ "title": "Nantes Carrots",
+ "category": "produce",
+ "summary": "Fresh bunches harvested in Saanich"
+ },
+ "primary_bin_id": "bunch",
+ "bins": [
+ {
+ "bin_id": "bunch",
+ "quantity": {
+ "amount": "1",
+ "unit": "each"
+ },
+ "price_per_canonical_unit": {
+ "amount": {
+ "amount": "4",
+ "currency": "CAD"
+ },
+ "quantity": {
+ "amount": "1",
+ "unit": "each"
+ }
+ }
+ }
+ ],
+ "inventory_available": "24",
+ "availability": {
+ "kind": "status",
+ "amount": {
+ "status": {
+ "kind": "active"
+ }
+ }
+ },
+ "delivery_method": {
+ "kind": "pickup"
+ },
+ "location": {
+ "primary": "Saanich Peninsula",
+ "city": "Victoria",
+ "region": "BC",
+ "country": "CA",
+ "geohash": "c28hr"
+ }
+ }
+ },
+ "expected": {
+ "tags": [
+ [
+ "d",
+ "AAAAAAAAAAAAAAAAAAAAAg"
+ ],
+ [
+ "p",
+ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
+ ],
+ [
+ "a",
+ "30340:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:AAAAAAAAAAAAAAAAAAAAAA"
+ ],
+ [
+ "key",
+ "carrot-nantes"
+ ],
+ [
+ "title",
+ "Nantes Carrots"
+ ],
+ [
+ "category",
+ "produce"
+ ],
+ [
+ "summary",
+ "Fresh bunches harvested in Saanich"
+ ],
+ [
+ "published_at",
+ "1700000000"
+ ],
+ [
+ "radroots:primary_bin",
+ "bunch"
+ ],
+ [
+ "radroots:bin",
+ "bunch",
+ "1",
+ "each"
+ ],
+ [
+ "radroots:price",
+ "bunch",
+ "4",
+ "CAD",
+ "1",
+ "each"
+ ],
+ [
+ "price",
+ "4",
+ "CAD"
+ ],
+ [
+ "inventory",
+ "24"
+ ],
+ [
+ "status",
+ "active"
+ ],
+ [
+ "delivery",
+ "pickup"
+ ],
+ [
+ "location",
+ "Saanich Peninsula",
+ "Victoria",
+ "BC",
+ "CA"
+ ],
+ [
+ "g",
+ "c28hr"
+ ]
+ ]
+ }
+ },
+ {
+ "id": "operational_listing_tags_full_empty_bins_002",
+ "kind": "operational_listing_tags_full",
+ "input": {
+ "listing": {
+ "d_tag": "AAAAAAAAAAAAAAAAAAAAAw",
+ "farm": {
+ "pubkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
+ "d_tag": "AAAAAAAAAAAAAAAAAAAAAA"
+ },
+ "product": {
+ "key": "carrot-nantes",
+ "title": "Nantes Carrots",
+ "category": "produce"
+ },
+ "primary_bin_id": "bunch",
+ "bins": []
+ }
+ },
+ "expected": {
+ "error": {
+ "code": "empty_required_field",
+ "field": "bins"
+ }
+ }
+ }
+ ]
+}
diff --git a/crates/event_codec/tests/operational_listing_conformance.rs b/crates/event_codec/tests/operational_listing_conformance.rs
@@ -0,0 +1,419 @@
+#![cfg(feature = "serde_json")]
+
+use std::{borrow::Cow, collections::BTreeSet, fs, path::Path};
+
+use radroots_event::{
+ RadrootsEventEnvelope, RadrootsEventEnvelopeParts, contract::validate_event_contract_shape,
+ operational_listing::RadrootsOperationalListing,
+};
+use radroots_event_codec::{
+ error::EventEncodeError,
+ operational_listing::{
+ encode::{operational_listing_build_tags, to_wire_parts},
+ tags::operational_listing_tags_full,
+ },
+};
+use serde::{Deserialize, de::DeserializeOwned};
+use serde_json::{Map, Value};
+
+const CONTRACT_ID: &str = "radroots.operational_listing.published.v1";
+const WORKSPACE_CONTRACT_MARKER_PATH: &str = "../../contracts/manifest.toml";
+const PACKAGED_BUILD_TAGS: &str = include_str!("fixtures/operational_listing_build_tags.v1.json");
+const WORKSPACE_BUILD_TAGS_PATH: &str =
+ "../../contracts/conformance/vectors/operational_listing/build_tags.v1.json";
+const PACKAGED_BUILD_DRAFT: &str = include_str!("fixtures/operational_listing_build_draft.v1.json");
+const WORKSPACE_BUILD_DRAFT_PATH: &str =
+ "../../contracts/conformance/vectors/operational_listing/build_draft.v1.json";
+const PACKAGED_FULL_TAGS: &str = include_str!("fixtures/operational_listing_tags_full.v1.json");
+const WORKSPACE_FULL_TAGS_PATH: &str =
+ "../../contracts/conformance/vectors/events/operational_listing_tags_full.v1.json";
+
+#[derive(Debug, Deserialize)]
+#[serde(deny_unknown_fields)]
+struct Suite<T> {
+ suite: String,
+ contract_version: String,
+ vectors: Vec<Vector<T>>,
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(deny_unknown_fields)]
+struct Vector<T> {
+ id: String,
+ kind: String,
+ input: ListingInput,
+ expected: T,
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(deny_unknown_fields)]
+struct ListingInput {
+ listing: Value,
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(untagged)]
+enum TagsExpected {
+ Success(TagsSuccess),
+ Failure(EncodeFailure),
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(deny_unknown_fields)]
+struct TagsSuccess {
+ tags: Vec<Vec<String>>,
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(untagged)]
+enum DraftExpected {
+ Success(DraftSuccess),
+ Failure(EncodeFailure),
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(deny_unknown_fields)]
+struct DraftSuccess {
+ wire_parts: WirePartsExpected,
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(deny_unknown_fields)]
+struct WirePartsExpected {
+ kind: u32,
+ content: String,
+ tags: Vec<Vec<String>>,
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(deny_unknown_fields)]
+struct EncodeFailure {
+ error: EncodeErrorExpected,
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(deny_unknown_fields)]
+struct EncodeErrorExpected {
+ code: String,
+ field: String,
+}
+
+#[test]
+fn checked_in_operational_listing_build_tags_vectors_execute() {
+ let suite: Suite<TagsExpected> = parse_suite(
+ PACKAGED_BUILD_TAGS,
+ WORKSPACE_BUILD_TAGS_PATH,
+ "operational_listing",
+ &[
+ "operational_listing_build_tags_empty_bins_002",
+ "operational_listing_build_tags_victoria_carrots_001",
+ ],
+ );
+
+ for vector in &suite.vectors {
+ let listing = typed_listing(&vector.input.listing, &vector.id);
+ assert_eq!(
+ vector.kind, "operational_listing.build_tags",
+ "{}",
+ vector.id
+ );
+ match &vector.expected {
+ TagsExpected::Success(expected) => {
+ let actual = operational_listing_build_tags(&listing)
+ .unwrap_or_else(|error| panic!("{} failed: {error}", vector.id));
+ assert_eq!(actual, expected.tags, "{}", vector.id);
+ }
+ TagsExpected::Failure(expected) => {
+ let error = operational_listing_build_tags(&listing)
+ .expect_err("invalid build-tags vector must fail");
+ assert_encode_error(&vector.id, &error, &expected.error);
+ }
+ }
+ }
+
+ let mut misspelled = suite.vectors[0].input.listing.clone();
+ misspelled["product"]["summmary"] = Value::String("misspelled optional field".to_string());
+ assert_eq!(
+ validate_listing_fixture_keys(&misspelled),
+ Err("listing.product contains unknown key `summmary`".to_string())
+ );
+}
+
+#[test]
+fn checked_in_operational_listing_build_draft_vectors_execute() {
+ let suite: Suite<DraftExpected> = parse_suite(
+ PACKAGED_BUILD_DRAFT,
+ WORKSPACE_BUILD_DRAFT_PATH,
+ "operational_listing",
+ &[
+ "operational_listing_build_draft_empty_bins_002",
+ "operational_listing_build_draft_victoria_carrots_001",
+ ],
+ );
+
+ for vector in &suite.vectors {
+ let listing = typed_listing(&vector.input.listing, &vector.id);
+ assert_eq!(
+ vector.kind, "operational_listing.build_draft",
+ "{}",
+ vector.id
+ );
+ match &vector.expected {
+ DraftExpected::Success(expected) => {
+ let actual = to_wire_parts(&listing)
+ .unwrap_or_else(|error| panic!("{} failed: {error}", vector.id));
+ assert_eq!(actual.kind, expected.wire_parts.kind, "{}", vector.id);
+ assert_eq!(actual.content, expected.wire_parts.content, "{}", vector.id);
+ assert_eq!(actual.tags, expected.wire_parts.tags, "{}", vector.id);
+ }
+ DraftExpected::Failure(expected) => {
+ let error =
+ to_wire_parts(&listing).expect_err("invalid build-draft vector must fail");
+ assert_encode_error(&vector.id, &error, &expected.error);
+ }
+ }
+ }
+}
+
+#[test]
+fn checked_in_operational_listing_full_event_tag_vectors_execute() {
+ let suite: Suite<TagsExpected> = parse_suite(
+ PACKAGED_FULL_TAGS,
+ WORKSPACE_FULL_TAGS_PATH,
+ "events",
+ &[
+ "operational_listing_tags_full_empty_bins_002",
+ "operational_listing_tags_full_victoria_carrots_001",
+ ],
+ );
+
+ for vector in &suite.vectors {
+ let listing = typed_listing(&vector.input.listing, &vector.id);
+ assert_eq!(
+ vector.kind, "operational_listing_tags_full",
+ "{}",
+ vector.id
+ );
+ match &vector.expected {
+ TagsExpected::Success(expected) => {
+ let actual = operational_listing_tags_full(&listing)
+ .unwrap_or_else(|error| panic!("{} failed: {error}", vector.id));
+ assert_eq!(actual, expected.tags, "{}", vector.id);
+
+ let wire = to_wire_parts(&listing)
+ .unwrap_or_else(|error| panic!("{} draft failed: {error}", vector.id));
+ assert_eq!(wire.tags, actual, "{}", vector.id);
+ let event = RadrootsEventEnvelope::new(RadrootsEventEnvelopeParts {
+ id: "b".repeat(64),
+ author: listing.farm.pubkey.clone(),
+ created_at: listing.published_at.unwrap_or_default(),
+ kind: wire.kind,
+ tags: wire.tags,
+ content: wire.content,
+ sig: "c".repeat(128),
+ })
+ .unwrap_or_else(|error| panic!("{} envelope failed: {error}", vector.id));
+ validate_event_contract_shape(&event, CONTRACT_ID)
+ .unwrap_or_else(|error| panic!("{} contract failed: {error:?}", vector.id));
+ }
+ TagsExpected::Failure(expected) => {
+ let error = operational_listing_tags_full(&listing)
+ .expect_err("invalid full-tags vector must fail");
+ assert_encode_error(&vector.id, &error, &expected.error);
+ }
+ }
+ }
+}
+
+fn parse_suite<T: DeserializeOwned>(
+ packaged: &'static str,
+ workspace_relative: &str,
+ expected_suite: &str,
+ expected_ids: &[&str],
+) -> Suite<T> {
+ let vectors = conformance_vectors(packaged, workspace_relative);
+ let suite: Suite<T> = serde_json::from_str(&vectors)
+ .unwrap_or_else(|error| panic!("{workspace_relative} must parse: {error}"));
+ assert_eq!(suite.suite, expected_suite, "{workspace_relative}");
+ assert_eq!(suite.contract_version, "1.0.0", "{workspace_relative}");
+
+ let actual_ids = suite
+ .vectors
+ .iter()
+ .map(|vector| vector.id.as_str())
+ .collect::<BTreeSet<_>>();
+ assert_eq!(
+ actual_ids.len(),
+ suite.vectors.len(),
+ "{workspace_relative} contains duplicate vector ids"
+ );
+ let expected_ids = expected_ids.iter().copied().collect::<BTreeSet<_>>();
+ assert_eq!(
+ actual_ids, expected_ids,
+ "{workspace_relative} inventory drift"
+ );
+ suite
+}
+
+fn conformance_vectors(packaged: &'static str, workspace_relative: &str) -> Cow<'static, str> {
+ let workspace_path = Path::new(env!("CARGO_MANIFEST_DIR")).join(workspace_relative);
+ match fs::read_to_string(&workspace_path) {
+ Ok(canonical) => {
+ assert_eq!(
+ canonical,
+ packaged,
+ "packaged vectors must match {}",
+ workspace_path.display()
+ );
+ Cow::Owned(canonical)
+ }
+ Err(error)
+ if error.kind() == std::io::ErrorKind::NotFound
+ && !Path::new(env!("CARGO_MANIFEST_DIR"))
+ .join(WORKSPACE_CONTRACT_MARKER_PATH)
+ .is_file() =>
+ {
+ Cow::Borrowed(packaged)
+ }
+ Err(error) => panic!("failed to read {}: {error}", workspace_path.display()),
+ }
+}
+
+fn assert_encode_error(id: &str, actual: &EventEncodeError, expected: &EncodeErrorExpected) {
+ assert_eq!(actual.code(), expected.code, "{id}");
+ match actual {
+ EventEncodeError::EmptyRequiredField(field) => {
+ assert_eq!(*field, expected.field, "{id}");
+ }
+ other => panic!("{id} returned unexpected encode error {other:?}"),
+ }
+}
+
+fn typed_listing(raw: &Value, id: &str) -> RadrootsOperationalListing {
+ validate_listing_fixture_keys(raw)
+ .unwrap_or_else(|error| panic!("{id} fixture schema failed: {error}"));
+ serde_json::from_value(raw.clone())
+ .unwrap_or_else(|error| panic!("{id} listing must deserialize: {error}"))
+}
+
+fn validate_listing_fixture_keys(raw: &Value) -> Result<(), String> {
+ let listing = fixture_object(raw, "listing")?;
+ assert_allowed_keys(
+ listing,
+ &[
+ "d_tag",
+ "published_at",
+ "farm",
+ "product",
+ "primary_bin_id",
+ "bins",
+ "inventory_available",
+ "availability",
+ "delivery_method",
+ "location",
+ ],
+ "listing",
+ )?;
+
+ let farm = required_fixture_object(listing, "farm", "listing")?;
+ assert_allowed_keys(farm, &["pubkey", "d_tag"], "listing.farm")?;
+
+ let product = required_fixture_object(listing, "product", "listing")?;
+ assert_allowed_keys(
+ product,
+ &["key", "title", "category", "summary"],
+ "listing.product",
+ )?;
+
+ let bins = listing
+ .get("bins")
+ .and_then(Value::as_array)
+ .ok_or_else(|| "listing.bins must be an array".to_string())?;
+ for (index, raw_bin) in bins.iter().enumerate() {
+ let bin_path = format!("listing.bins[{index}]");
+ let bin = fixture_object(raw_bin, &bin_path)?;
+ assert_allowed_keys(
+ bin,
+ &["bin_id", "quantity", "price_per_canonical_unit"],
+ &bin_path,
+ )?;
+
+ let quantity = required_fixture_object(bin, "quantity", &bin_path)?;
+ assert_allowed_keys(
+ quantity,
+ &["amount", "unit"],
+ &format!("{bin_path}.quantity"),
+ )?;
+
+ let price = required_fixture_object(bin, "price_per_canonical_unit", &bin_path)?;
+ let price_path = format!("{bin_path}.price_per_canonical_unit");
+ assert_allowed_keys(price, &["amount", "quantity"], &price_path)?;
+ let money = required_fixture_object(price, "amount", &price_path)?;
+ assert_allowed_keys(
+ money,
+ &["amount", "currency"],
+ &format!("{price_path}.amount"),
+ )?;
+ let price_quantity = required_fixture_object(price, "quantity", &price_path)?;
+ assert_allowed_keys(
+ price_quantity,
+ &["amount", "unit"],
+ &format!("{price_path}.quantity"),
+ )?;
+ }
+
+ if let Some(raw_availability) = listing.get("availability") {
+ let availability = fixture_object(raw_availability, "listing.availability")?;
+ assert_allowed_keys(availability, &["kind", "amount"], "listing.availability")?;
+ let amount = required_fixture_object(availability, "amount", "listing.availability")?;
+ assert_allowed_keys(amount, &["status"], "listing.availability.amount")?;
+ let status = required_fixture_object(amount, "status", "listing.availability.amount")?;
+ assert_allowed_keys(status, &["kind"], "listing.availability.amount.status")?;
+ }
+
+ if let Some(raw_delivery) = listing.get("delivery_method") {
+ let delivery = fixture_object(raw_delivery, "listing.delivery_method")?;
+ assert_allowed_keys(delivery, &["kind"], "listing.delivery_method")?;
+ }
+
+ if let Some(raw_location) = listing.get("location") {
+ let location = fixture_object(raw_location, "listing.location")?;
+ assert_allowed_keys(
+ location,
+ &["primary", "city", "region", "country", "geohash"],
+ "listing.location",
+ )?;
+ }
+
+ Ok(())
+}
+
+fn fixture_object<'a>(raw: &'a Value, path: &str) -> Result<&'a Map<String, Value>, String> {
+ raw.as_object()
+ .ok_or_else(|| format!("{path} must be an object"))
+}
+
+fn required_fixture_object<'a>(
+ parent: &'a Map<String, Value>,
+ key: &str,
+ parent_path: &str,
+) -> Result<&'a Map<String, Value>, String> {
+ let path = format!("{parent_path}.{key}");
+ parent
+ .get(key)
+ .ok_or_else(|| format!("{path} is required"))
+ .and_then(|raw| fixture_object(raw, &path))
+}
+
+fn assert_allowed_keys(
+ object: &Map<String, Value>,
+ allowed: &[&str],
+ path: &str,
+) -> Result<(), String> {
+ for key in object.keys() {
+ if !allowed.contains(&key.as_str()) {
+ return Err(format!("{path} contains unknown key `{key}`"));
+ }
+ }
+ Ok(())
+}
diff --git a/crates/trade/tests/fixtures/parse_classified_listing_address.v1.json b/crates/trade/tests/fixtures/parse_classified_listing_address.v1.json
@@ -0,0 +1,101 @@
+{
+ "suite": "trade",
+ "contract_version": "1.0.0",
+ "vectors": [
+ {
+ "id": "trade_parse_classified_listing_address_canonical_001",
+ "kind": "trade.parse_classified_listing_address.valid",
+ "input": {
+ "listing_addr": "30402:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:AAAAAAAAAAAAAAAAAAAAAg"
+ },
+ "expected": {
+ "address": "30402:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:AAAAAAAAAAAAAAAAAAAAAg",
+ "kind": 30402,
+ "seller_pubkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
+ "listing_id": "AAAAAAAAAAAAAAAAAAAAAg"
+ }
+ },
+ {
+ "id": "trade_parse_classified_listing_address_normalized_002",
+ "kind": "trade.parse_classified_listing_address.valid",
+ "input": {
+ "listing_addr": "+030402:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA:AAAAAAAAAAAAAAAAAAAAAg"
+ },
+ "expected": {
+ "address": "30402:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:AAAAAAAAAAAAAAAAAAAAAg",
+ "kind": 30402,
+ "seller_pubkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
+ "listing_id": "AAAAAAAAAAAAAAAAAAAAAg"
+ }
+ },
+ {
+ "id": "trade_parse_classified_listing_address_wrong_kind_003",
+ "kind": "trade.parse_classified_listing_address.invalid",
+ "input": {
+ "listing_addr": "30403:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:AAAAAAAAAAAAAAAAAAAAAg"
+ },
+ "expected": {
+ "error": {
+ "kind": "unexpected_kind",
+ "expected": 30402,
+ "actual": 30403
+ },
+ "message": "identifier kind 30403 does not match required kind 30402"
+ }
+ },
+ {
+ "id": "trade_parse_classified_listing_address_malformed_format_004",
+ "kind": "trade.parse_classified_listing_address.invalid",
+ "input": {
+ "listing_addr": "30402"
+ },
+ "expected": {
+ "error": {
+ "kind": "invalid_format"
+ },
+ "message": "identifier has invalid format"
+ }
+ },
+ {
+ "id": "trade_parse_classified_listing_address_short_pubkey_005",
+ "kind": "trade.parse_classified_listing_address.invalid",
+ "input": {
+ "listing_addr": "30402:not_hex:AAAAAAAAAAAAAAAAAAAAAg"
+ },
+ "expected": {
+ "error": {
+ "kind": "invalid_length",
+ "expected": 64,
+ "actual": 7
+ },
+ "message": "identifier length 7 does not match required length 64"
+ }
+ },
+ {
+ "id": "trade_parse_classified_listing_address_non_hex_pubkey_006",
+ "kind": "trade.parse_classified_listing_address.invalid",
+ "input": {
+ "listing_addr": "30402:gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg:AAAAAAAAAAAAAAAAAAAAAg"
+ },
+ "expected": {
+ "error": {
+ "kind": "invalid_character"
+ },
+ "message": "identifier contains an invalid character"
+ }
+ },
+ {
+ "id": "trade_parse_classified_listing_address_invalid_d_tag_007",
+ "kind": "trade.parse_classified_listing_address.invalid",
+ "input": {
+ "listing_addr": "30402:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:bad d"
+ },
+ "expected": {
+ "error": {
+ "kind": "invalid_character"
+ },
+ "message": "identifier contains an invalid character"
+ }
+ }
+ ]
+}
diff --git a/crates/trade/tests/fixtures/validate_operational_listing_event.v1.json b/crates/trade/tests/fixtures/validate_operational_listing_event.v1.json
@@ -0,0 +1,222 @@
+{
+ "suite": "trade_validation",
+ "contract_version": "1.0.0",
+ "vectors": [
+ {
+ "id": "trade_validation_validate_operational_listing_event_valid_001",
+ "kind": "trade_validation.validate_operational_listing_event.valid",
+ "input": {
+ "event": {
+ "id": "6739ed38175521d1b8a64592ec3407332ee24449e1e7353fd8f721c0995b9d8f",
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "created_at": 1700000000,
+ "kind": 30402,
+ "tags": [
+ ["d", "AAAAAAAAAAAAAAAAAAAAAg"],
+ ["p", "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df"],
+ ["a", "30340:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:AAAAAAAAAAAAAAAAAAAAAA"],
+ ["key", "carrot-nantes"],
+ ["title", "Nantes Carrots"],
+ ["category", "produce"],
+ ["summary", "Fresh bunches harvested in Saanich"],
+ ["published_at", "1700000000"],
+ ["radroots:primary_bin", "bunch"],
+ ["radroots:bin", "bunch", "1", "each"],
+ ["radroots:price", "bunch", "4", "CAD", "1", "each"],
+ ["price", "4", "CAD"],
+ ["inventory", "24"],
+ ["status", "active"],
+ ["delivery", "pickup"],
+ ["location", "Saanich Peninsula", "Victoria", "BC", "CA"],
+ ["g", "c28hr"]
+ ],
+ "content": "# Nantes Carrots\n\nFresh bunches harvested in Saanich",
+ "sig": "ef3413c4ac4be3f748fcb51b3e5b9b03c590f5f814dbd6ab3f2f2c26dbc87eb1347cefbe97abacce60f0c4530848695e766d3a950350c72089813b3318a3f944"
+ }
+ },
+ "expected": {
+ "projection": {
+ "listing_id": "AAAAAAAAAAAAAAAAAAAAAg",
+ "listing_addr": "30402:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:AAAAAAAAAAAAAAAAAAAAAg",
+ "seller_pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "title": "Nantes Carrots",
+ "description": "Fresh bunches harvested in Saanich",
+ "product_type": "produce",
+ "primary_bin_id": "bunch",
+ "bin_quantity": {
+ "amount": "1",
+ "unit": "each"
+ },
+ "unit": "each",
+ "unit_price": {
+ "amount": "4",
+ "currency": "CAD"
+ },
+ "inventory_available": "24",
+ "availability": {
+ "kind": "status",
+ "amount": {
+ "status": {
+ "kind": "active"
+ }
+ }
+ },
+ "location": {
+ "primary": "Saanich Peninsula",
+ "city": "Victoria",
+ "region": "BC",
+ "country": "CA",
+ "geohash": "c28hr"
+ },
+ "delivery_method": {
+ "kind": "pickup"
+ },
+ "listing": {
+ "d_tag": "AAAAAAAAAAAAAAAAAAAAAg",
+ "published_at": 1700000000,
+ "farm": {
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "d_tag": "AAAAAAAAAAAAAAAAAAAAAA"
+ },
+ "product": {
+ "key": "carrot-nantes",
+ "title": "Nantes Carrots",
+ "category": "produce",
+ "summary": "Fresh bunches harvested in Saanich",
+ "process": null,
+ "lot": null,
+ "location": null,
+ "profile": null,
+ "year": null
+ },
+ "primary_bin_id": "bunch",
+ "bins": [
+ {
+ "bin_id": "bunch",
+ "quantity": {
+ "amount": "1",
+ "unit": "each"
+ },
+ "price_per_canonical_unit": {
+ "amount": {
+ "amount": "4",
+ "currency": "CAD"
+ },
+ "quantity": {
+ "amount": "1",
+ "unit": "each"
+ }
+ },
+ "display_amount": null,
+ "display_unit": null,
+ "display_label": null,
+ "display_price": null,
+ "display_price_unit": null
+ }
+ ],
+ "resource_area": null,
+ "plot": null,
+ "discounts": null,
+ "inventory_available": "24",
+ "availability": {
+ "kind": "status",
+ "amount": {
+ "status": {
+ "kind": "active"
+ }
+ }
+ },
+ "delivery_method": {
+ "kind": "pickup"
+ },
+ "location": {
+ "primary": "Saanich Peninsula",
+ "city": "Victoria",
+ "region": "BC",
+ "country": "CA",
+ "geohash": "c28hr"
+ },
+ "images": null
+ }
+ }
+ }
+ },
+ {
+ "id": "trade_validation_validate_operational_listing_event_invalid_seller_002",
+ "kind": "trade_validation.validate_operational_listing_event.invalid",
+ "input": {
+ "event": {
+ "id": "294177fd3f698947e8f1be9f1f3429c87d87d17a643b1c3666674de7bbe6eb20",
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "created_at": 1700000000,
+ "kind": 30402,
+ "tags": [
+ ["d", "AAAAAAAAAAAAAAAAAAAAAg"],
+ ["p", "e0266e3cfb0d2886f91c73f5f868f3b98273713e5fcd97c081663f5518a4b3af"],
+ ["a", "30340:e0266e3cfb0d2886f91c73f5f868f3b98273713e5fcd97c081663f5518a4b3af:AAAAAAAAAAAAAAAAAAAAAA"],
+ ["key", "carrot-nantes"],
+ ["title", "Nantes Carrots"],
+ ["category", "produce"],
+ ["summary", "Fresh bunches harvested in Saanich"],
+ ["published_at", "1700000000"],
+ ["radroots:primary_bin", "bunch"],
+ ["radroots:bin", "bunch", "1", "each"],
+ ["radroots:price", "bunch", "4", "CAD", "1", "each"],
+ ["price", "4", "CAD"],
+ ["inventory", "24"],
+ ["status", "active"],
+ ["delivery", "pickup"],
+ ["location", "Saanich Peninsula", "Victoria", "BC", "CA"],
+ ["g", "c28hr"]
+ ],
+ "content": "# Nantes Carrots\n\nFresh bunches harvested in Saanich",
+ "sig": "0b12724932817061d949d1d4543beaa90b27b08f09f63422871dda01ee692b26c4ff9ef66b31ea4500b76d1c8e45c2126c96b7dc17b8f242b29b97a5a7294b73"
+ }
+ },
+ "expected": {
+ "error": {
+ "kind": "invalid_seller"
+ },
+ "message": "listing author does not match farm pubkey"
+ }
+ },
+ {
+ "id": "trade_validation_validate_operational_listing_event_missing_inventory_003",
+ "kind": "trade_validation.validate_operational_listing_event.invalid",
+ "input": {
+ "event": {
+ "id": "b2d934d5b9a089a500c7c2a76fb1486b4abf2dd4b9334e3dc00114884818e77a",
+ "pubkey": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
+ "created_at": 1700000000,
+ "kind": 30402,
+ "tags": [
+ ["d", "AAAAAAAAAAAAAAAAAAAAAg"],
+ ["p", "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df"],
+ ["a", "30340:585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df:AAAAAAAAAAAAAAAAAAAAAA"],
+ ["key", "carrot-nantes"],
+ ["title", "Nantes Carrots"],
+ ["category", "produce"],
+ ["summary", "Fresh bunches harvested in Saanich"],
+ ["published_at", "1700000000"],
+ ["radroots:primary_bin", "bunch"],
+ ["radroots:bin", "bunch", "1", "each"],
+ ["radroots:price", "bunch", "4", "CAD", "1", "each"],
+ ["price", "4", "CAD"],
+ ["status", "active"],
+ ["delivery", "pickup"],
+ ["location", "Saanich Peninsula", "Victoria", "BC", "CA"],
+ ["g", "c28hr"]
+ ],
+ "content": "# Nantes Carrots\n\nFresh bunches harvested in Saanich",
+ "sig": "3504a764b97f8fd2915281e8c110247979da5f62743967227711a08c08de5a87ad79500def9da29730e12175dbe656f75b75fe2dae4a4e25c7f989aecd3520ef"
+ }
+ },
+ "expected": {
+ "error": {
+ "kind": "missing_inventory"
+ },
+ "message": "missing listing inventory"
+ }
+ }
+ ]
+}
diff --git a/crates/trade/tests/operational_listing_conformance.rs b/crates/trade/tests/operational_listing_conformance.rs
@@ -0,0 +1,316 @@
+#![cfg(all(feature = "std", feature = "serde", feature = "serde_json"))]
+
+use nostr::secp256k1::Message;
+use nostr::{Event as NostrEvent, JsonUtil, Keys, SECP256K1};
+use radroots_event::{RadrootsNip01EventWire, ids::RadrootsIdParseError};
+use radroots_nostr::prelude::radroots_event_from_nostr;
+use radroots_trade::operational_listing::{
+ parse_classified_listing_address, validation::validate_operational_listing_event,
+};
+use serde::Deserialize;
+use serde_json::{Value, json};
+use std::{borrow::Cow, collections::BTreeSet, fs, path::Path};
+
+const SIGNING_SECRET_KEY: &str = "10c5304d6c9ae3a1a16f7860f1cc8f5e3a76225a2663b3a989a0d775919b7df5";
+const PACKAGED_ADDRESS_VECTORS: &str =
+ include_str!("fixtures/parse_classified_listing_address.v1.json");
+const PACKAGED_VALIDATION_VECTORS: &str =
+ include_str!("fixtures/validate_operational_listing_event.v1.json");
+const WORKSPACE_ADDRESS_VECTOR_PATH: &str =
+ "../../contracts/conformance/vectors/trade/parse_classified_listing_address.v1.json";
+const WORKSPACE_VALIDATION_VECTOR_PATH: &str = "../../contracts/conformance/vectors/trade_validation/validate_operational_listing_event.v1.json";
+const WORKSPACE_CONTRACT_MARKER_PATH: &str = "../../contracts/manifest.toml";
+
+const ADDRESS_VECTOR_IDS: [&str; 7] = [
+ "trade_parse_classified_listing_address_canonical_001",
+ "trade_parse_classified_listing_address_normalized_002",
+ "trade_parse_classified_listing_address_wrong_kind_003",
+ "trade_parse_classified_listing_address_malformed_format_004",
+ "trade_parse_classified_listing_address_short_pubkey_005",
+ "trade_parse_classified_listing_address_non_hex_pubkey_006",
+ "trade_parse_classified_listing_address_invalid_d_tag_007",
+];
+
+const VALIDATION_VECTOR_IDS: [&str; 3] = [
+ "trade_validation_validate_operational_listing_event_valid_001",
+ "trade_validation_validate_operational_listing_event_invalid_seller_002",
+ "trade_validation_validate_operational_listing_event_missing_inventory_003",
+];
+
+#[derive(Debug, Deserialize)]
+#[serde(deny_unknown_fields)]
+struct Suite {
+ suite: String,
+ contract_version: String,
+ vectors: Vec<Vector>,
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(deny_unknown_fields)]
+struct Vector {
+ id: String,
+ kind: String,
+ input: Value,
+ expected: Value,
+}
+
+#[test]
+fn classified_listing_address_vectors_execute_against_public_api() {
+ let raw = conformance_vectors(
+ PACKAGED_ADDRESS_VECTORS,
+ WORKSPACE_ADDRESS_VECTOR_PATH,
+ "Classified Listing address",
+ );
+ let suite: Suite = serde_json::from_str(&raw).expect("address vectors must parse");
+ assert_suite(&suite, "trade", &ADDRESS_VECTOR_IDS);
+
+ for vector in &suite.vectors {
+ match vector.kind.as_str() {
+ "trade.parse_classified_listing_address.valid" => address_valid(vector),
+ "trade.parse_classified_listing_address.invalid" => address_invalid(vector),
+ kind => panic!("{} uses unsupported vector kind {kind}", vector.id),
+ }
+ }
+}
+
+#[test]
+fn operational_listing_validation_vectors_execute_verified_nip01_events() {
+ let raw = conformance_vectors(
+ PACKAGED_VALIDATION_VECTORS,
+ WORKSPACE_VALIDATION_VECTOR_PATH,
+ "Operational Listing validation",
+ );
+ let suite: Suite = serde_json::from_str(&raw).expect("validation vectors must parse");
+ assert_suite(&suite, "trade_validation", &VALIDATION_VECTOR_IDS);
+ let keys = Keys::parse(SIGNING_SECRET_KEY).expect("fixed signing key must parse");
+
+ for vector in &suite.vectors {
+ let event = verified_event(vector, &keys);
+ match vector.kind.as_str() {
+ "trade_validation.validate_operational_listing_event.valid" => {
+ validation_valid(vector, &event)
+ }
+ "trade_validation.validate_operational_listing_event.invalid" => {
+ validation_invalid(vector, &event)
+ }
+ kind => panic!("{} uses unsupported vector kind {kind}", vector.id),
+ }
+ }
+}
+
+fn conformance_vectors(
+ packaged: &'static str,
+ workspace_relative: &str,
+ label: &str,
+) -> Cow<'static, str> {
+ let workspace_path = Path::new(env!("CARGO_MANIFEST_DIR")).join(workspace_relative);
+ match fs::read_to_string(&workspace_path) {
+ Ok(canonical) => {
+ assert_eq!(
+ canonical,
+ packaged,
+ "packaged {label} vectors must match {}",
+ workspace_path.display()
+ );
+ Cow::Owned(canonical)
+ }
+ Err(error)
+ if error.kind() == std::io::ErrorKind::NotFound
+ && !Path::new(env!("CARGO_MANIFEST_DIR"))
+ .join(WORKSPACE_CONTRACT_MARKER_PATH)
+ .is_file() =>
+ {
+ Cow::Borrowed(packaged)
+ }
+ Err(error) => panic!("failed to read {}: {error}", workspace_path.display()),
+ }
+}
+
+fn assert_suite(suite: &Suite, expected_suite: &str, expected_ids: &[&str]) {
+ assert_eq!(suite.suite, expected_suite);
+ assert_eq!(suite.contract_version, "1.0.0");
+ assert_eq!(suite.vectors.len(), expected_ids.len());
+
+ let actual: BTreeSet<&str> = suite
+ .vectors
+ .iter()
+ .map(|vector| vector.id.as_str())
+ .collect();
+ let expected: BTreeSet<&str> = expected_ids.iter().copied().collect();
+ assert_eq!(actual, expected, "conformance vector inventory drift");
+}
+
+fn address_valid(vector: &Vector) {
+ assert_object_keys(&vector.input, &["listing_addr"], "input", &vector.id);
+ assert_object_keys(
+ &vector.expected,
+ &["address", "kind", "seller_pubkey", "listing_id"],
+ "expected",
+ &vector.id,
+ );
+ let parsed = parse_classified_listing_address(input_str(vector, "listing_addr"))
+ .unwrap_or_else(|error| panic!("{} failed: {error}", vector.id));
+ let actual = json!({
+ "address": parsed.address.as_str(),
+ "kind": parsed.kind,
+ "seller_pubkey": parsed.seller_pubkey.as_str(),
+ "listing_id": parsed.listing_id.as_str(),
+ });
+ assert_eq!(actual, vector.expected, "{}", vector.id);
+}
+
+fn address_invalid(vector: &Vector) {
+ assert_object_keys(&vector.input, &["listing_addr"], "input", &vector.id);
+ assert_object_keys(
+ &vector.expected,
+ &["error", "message"],
+ "expected",
+ &vector.id,
+ );
+ let error = parse_classified_listing_address(input_str(vector, "listing_addr"))
+ .expect_err("invalid Classified Listing address vector must fail");
+ let actual = json!({
+ "error": address_error_value(&error),
+ "message": error.to_string(),
+ });
+ assert_eq!(actual, vector.expected, "{}", vector.id);
+}
+
+fn address_error_value(error: &RadrootsIdParseError) -> Value {
+ match error {
+ RadrootsIdParseError::Empty => json!({ "kind": "empty" }),
+ RadrootsIdParseError::InvalidFormat => json!({ "kind": "invalid_format" }),
+ RadrootsIdParseError::InvalidLength { expected, actual } => json!({
+ "kind": "invalid_length",
+ "expected": expected,
+ "actual": actual,
+ }),
+ RadrootsIdParseError::InvalidCharacter => json!({ "kind": "invalid_character" }),
+ RadrootsIdParseError::UnexpectedKind { expected, actual } => json!({
+ "kind": "unexpected_kind",
+ "expected": expected,
+ "actual": actual,
+ }),
+ RadrootsIdParseError::TooLong { max, actual } => json!({
+ "kind": "too_long",
+ "max": max,
+ "actual": actual,
+ }),
+ }
+}
+
+fn verified_event(vector: &Vector, keys: &Keys) -> radroots_event::RadrootsEventEnvelope {
+ assert_object_keys(&vector.input, &["event"], "input", &vector.id);
+ let event_value = vector
+ .input
+ .get("event")
+ .unwrap_or_else(|| panic!("{} is missing input.event", vector.id));
+ assert_object_keys(
+ event_value,
+ &[
+ "id",
+ "pubkey",
+ "created_at",
+ "kind",
+ "tags",
+ "content",
+ "sig",
+ ],
+ "input.event",
+ &vector.id,
+ );
+ let raw = serde_json::to_string(event_value).expect("event vector must serialize");
+ let event = NostrEvent::from_json(&raw)
+ .unwrap_or_else(|error| panic!("{} is not a NIP-01 event: {error}", vector.id));
+ let expected_id = value_str(event_value, "id", &vector.id);
+ let expected_signature = value_str(event_value, "sig", &vector.id);
+
+ assert_eq!(event.id.to_string(), expected_id, "{}", vector.id);
+ assert_eq!(event.sig.to_string(), expected_signature, "{}", vector.id);
+ assert_eq!(
+ event.pubkey,
+ keys.public_key(),
+ "{} signer drift",
+ vector.id
+ );
+ let message = Message::from_digest(event.id.to_bytes());
+ let deterministic_signature =
+ SECP256K1.sign_schnorr_no_aux_rand(&message, keys.key_pair(SECP256K1));
+ assert_eq!(
+ event.sig, deterministic_signature,
+ "{} deterministic signature drift",
+ vector.id
+ );
+ event
+ .verify()
+ .unwrap_or_else(|error| panic!("{} failed NIP-01 verification: {error}", vector.id));
+
+ let wire = RadrootsNip01EventWire::parse_json(&raw)
+ .unwrap_or_else(|error| panic!("{} failed Radroots wire parsing: {error}", vector.id));
+ assert!(
+ wire.extra.is_empty(),
+ "{} has extra NIP-01 fields",
+ vector.id
+ );
+ assert_eq!(wire.id, expected_id, "{}", vector.id);
+ assert_eq!(wire.sig, expected_signature, "{}", vector.id);
+ wire.verify_id()
+ .unwrap_or_else(|error| panic!("{} failed Radroots ID verification: {error}", vector.id));
+ let envelope = wire
+ .into_envelope()
+ .unwrap_or_else(|error| panic!("{} failed envelope conversion: {error}", vector.id));
+ let adapted = radroots_event_from_nostr(&event)
+ .unwrap_or_else(|error| panic!("{} failed Nostr adapter conversion: {error}", vector.id));
+ assert_eq!(envelope, adapted, "{} conversion drift", vector.id);
+ envelope
+}
+
+fn validation_valid(vector: &Vector, event: &radroots_event::RadrootsEventEnvelope) {
+ assert_object_keys(&vector.expected, &["projection"], "expected", &vector.id);
+ let projection = validate_operational_listing_event(event)
+ .unwrap_or_else(|error| panic!("{} failed: {error}", vector.id));
+ assert_eq!(
+ projection.listing.farm.pubkey,
+ event.author_str(),
+ "{} decoded farm author drift",
+ vector.id
+ );
+ let actual = serde_json::to_value(projection).expect("projection must serialize");
+ assert_eq!(actual, vector.expected["projection"], "{}", vector.id);
+}
+
+fn validation_invalid(vector: &Vector, event: &radroots_event::RadrootsEventEnvelope) {
+ assert_object_keys(
+ &vector.expected,
+ &["error", "message"],
+ "expected",
+ &vector.id,
+ );
+ let error = validate_operational_listing_event(event)
+ .expect_err("invalid Operational Listing validation vector must fail");
+ let actual = json!({
+ "error": serde_json::to_value(&error).expect("validation error must serialize"),
+ "message": error.to_string(),
+ });
+ assert_eq!(actual, vector.expected, "{}", vector.id);
+}
+
+fn input_str<'a>(vector: &'a Vector, key: &str) -> &'a str {
+ value_str(&vector.input, key, &vector.id)
+}
+
+fn value_str<'a>(value: &'a Value, key: &str, vector_id: &str) -> &'a str {
+ value
+ .get(key)
+ .and_then(Value::as_str)
+ .unwrap_or_else(|| panic!("{vector_id} is missing string field {key}"))
+}
+
+fn assert_object_keys(value: &Value, expected: &[&str], path: &str, vector_id: &str) {
+ let object = value
+ .as_object()
+ .unwrap_or_else(|| panic!("{vector_id} {path} must be an object"));
+ let actual = object.keys().map(String::as_str).collect::<BTreeSet<_>>();
+ let expected = expected.iter().copied().collect::<BTreeSet<_>>();
+ assert_eq!(actual, expected, "{vector_id} {path} key drift");
+}
diff --git a/tools/xtask/src/contract.rs b/tools/xtask/src/contract.rs
@@ -30,7 +30,7 @@ const REPLICA_CONTRACT_NAME: &str = "radroots_replica_contract";
const REPLICA_TRANSFER_CONSTANT: &str = "RADROOTS_REPLICA_TRANSFER_VERSION";
const REPLICA_TRANSFER_VERSION: u32 = 2;
const VENDORED_WORKSPACE_MEMBER_RELATIVE: &str = "crates/libsqlite3_sys_3_53_3";
-const CONFORMANCE_VECTOR_MIRRORS: [(&str, &str); 9] = [
+const CONFORMANCE_VECTOR_MIRRORS: [(&str, &str); 14] = [
(
"contracts/conformance/vectors/blossom/bud11_claims.v1.json",
"crates/blossom/tests/fixtures/bud11_claims.v1.json",
@@ -52,6 +52,18 @@ const CONFORMANCE_VECTOR_MIRRORS: [(&str, &str); 9] = [
"crates/event_codec/tests/fixtures/calendar_radroots_profile.v1.json",
),
(
+ "contracts/conformance/vectors/events/operational_listing_tags_full.v1.json",
+ "crates/event_codec/tests/fixtures/operational_listing_tags_full.v1.json",
+ ),
+ (
+ "contracts/conformance/vectors/operational_listing/build_draft.v1.json",
+ "crates/event_codec/tests/fixtures/operational_listing_build_draft.v1.json",
+ ),
+ (
+ "contracts/conformance/vectors/operational_listing/build_tags.v1.json",
+ "crates/event_codec/tests/fixtures/operational_listing_build_tags.v1.json",
+ ),
+ (
"contracts/conformance/vectors/operational_listing/parse_event.v1.json",
"crates/event_codec/tests/fixtures/operational_listing_parse_event.v1.json",
),
@@ -67,6 +79,14 @@ const CONFORMANCE_VECTOR_MIRRORS: [(&str, &str); 9] = [
"contracts/conformance/vectors/post/verified_profiles.v1.json",
"crates/event_codec/tests/fixtures/post_verified_profiles.v1.json",
),
+ (
+ "contracts/conformance/vectors/trade/parse_classified_listing_address.v1.json",
+ "crates/trade/tests/fixtures/parse_classified_listing_address.v1.json",
+ ),
+ (
+ "contracts/conformance/vectors/trade_validation/validate_operational_listing_event.v1.json",
+ "crates/trade/tests/fixtures/validate_operational_listing_event.v1.json",
+ ),
];
const KNOWLEDGE_MVP_SUPPORT_CONTRACT_IDS: [&str; 8] = [
"radroots.wiki.article.v1",
@@ -6579,14 +6599,7 @@ vector = "contracts/conformance/vectors/operational_listing/build_draft.v1.json"
.join("metadata.v1.json"),
SYNTHETIC_CONFORMANCE_VECTOR,
);
- write_file(
- &root
- .join("contracts")
- .join("conformance")
- .join("vectors")
- .join("operational_listing")
- .join("build_draft.v1.json"),
- r#"{
+ let operational_listing_vector = r#"{
"suite": "operational_listing",
"contract_version": "1.0.0",
"vectors": [
@@ -6598,7 +6611,24 @@ vector = "contracts/conformance/vectors/operational_listing/build_draft.v1.json"
}
]
}
-"#,
+"#;
+ write_file(
+ &root
+ .join("contracts")
+ .join("conformance")
+ .join("vectors")
+ .join("operational_listing")
+ .join("build_draft.v1.json"),
+ operational_listing_vector,
+ );
+ write_file(
+ &root
+ .join("crates")
+ .join("event_codec")
+ .join("tests")
+ .join("fixtures")
+ .join("operational_listing_build_draft.v1.json"),
+ operational_listing_vector,
);
}