diff --git a/.vscode/settings.json b/.vscode/settings.json index 32d5af0..2bd54da 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -6,10 +6,12 @@ "happ", "Holochain", "Holoom", + "msgpack", "PKCE", "pubkey", "Solana", "tryorama", + "typeshare", "workdir", "zome", "zomes" diff --git a/Cargo.lock b/Cargo.lock index 7308c73..d3fc8f0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3083,6 +3083,13 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "hdk_utils" +version = "0.0.1" +dependencies = [ + "hdk", +] + [[package]] name = "headers" version = "0.3.9" @@ -3834,6 +3841,7 @@ dependencies = [ "holoom_types", "serde", "tokio", + "username_registry_coordinator", "username_registry_utils", "username_registry_validation", ] @@ -6382,6 +6390,7 @@ version = "0.0.1" dependencies = [ "hdk", "serde", + "typeshare", ] [[package]] @@ -8529,6 +8538,26 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +[[package]] +name = "typeshare" +version = "1.0.3" +source = "git+https://github.com/8e8b2c/typeshare?rev=ccb29d34411824f2758109c3e844a8f8bc147b63#ccb29d34411824f2758109c3e844a8f8bc147b63" +dependencies = [ + "chrono", + "serde", + "serde_json", + "typeshare-annotation", +] + +[[package]] +name = "typeshare-annotation" +version = "1.0.4" +source = "git+https://github.com/8e8b2c/typeshare?rev=ccb29d34411824f2758109c3e844a8f8bc147b63#ccb29d34411824f2758109c3e844a8f8bc147b63" +dependencies = [ + "quote", + "syn 2.0.52", +] + [[package]] name = "ucd-trie" version = "0.1.6" @@ -8682,11 +8711,39 @@ version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" +[[package]] +name = "user_metadata_handlers" +version = "0.0.1" +dependencies = [ + "hdk", + "hdk_utils", + "serde", + "user_metadata_types", +] + +[[package]] +name = "user_metadata_types" +version = "0.0.1" +dependencies = [ + "hdi", + "serde", + "typeshare", +] + +[[package]] +name = "user_metadata_validation" +version = "0.0.1" +dependencies = [ + "hdi", + "serde", + "typeshare", + "user_metadata_types", +] + [[package]] name = "username_registry_coordinator" version = "0.0.1" dependencies = [ - "bincode", "hdk", "hex", "holoom_types", @@ -8694,6 +8751,9 @@ dependencies = [ "jaq_wrapper", "serde", "serde_json", + "typeshare", + "user_metadata_handlers", + "user_metadata_types", "username_registry_integrity", "username_registry_utils", "username_registry_validation", @@ -8706,6 +8766,10 @@ dependencies = [ "hdi", "holoom_types", "serde", + "serde_json", + "typeshare", + "user_metadata_types", + "user_metadata_validation", "username_registry_validation", ] @@ -8713,7 +8777,6 @@ dependencies = [ name = "username_registry_utils" version = "0.0.1" dependencies = [ - "bincode", "bs58 0.5.0", "ed25519-dalek", "hdi", @@ -8727,7 +8790,6 @@ dependencies = [ name = "username_registry_validation" version = "0.0.1" dependencies = [ - "bincode", "bs58 0.5.0", "ed25519-dalek", "hdi", diff --git a/Cargo.toml b/Cargo.toml index 726f1ce..774dcde 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,12 @@ opt-level = "z" [workspace] resolver = "2" -members = ["crates/*"] +members = [ + "features/*/types", + "features/*/validation", + "features/*/handlers", + "crates/*", +] [workspace.dependencies] hdi = "=0.5.0-dev.9" @@ -14,7 +19,6 @@ hdk = "=0.4.0-dev.10" holo_hash = { version = "=0.4.0-dev.8", features = ["encoding"] } serde = "1" serde_json = "1" -bincode = "1.3.3" hex = "0.4.3" alloy-primitives = { version = "0.6.3", features = ["serde", "k256"] } ed25519-dalek = { version = "2.1.1", features = ["serde"] } @@ -23,6 +27,19 @@ ethers-signers = "0.6.2" holochain = { version = "=0.4.0-dev.12", default-features = false, features = [ "test_utils", ] } +typeshare = { git = "https://github.com/8e8b2c/typeshare", rev = "ccb29d34411824f2758109c3e844a8f8bc147b63", default-features = false } + +[workspace.dependencies.hdk_utils] +path = "crates/hdk_utils" + +[workspace.dependencies.user_metadata_types] +path = "features/user_metadata/types" + +[workspace.dependencies.user_metadata_validation] +path = "features/user_metadata/validation" + +[workspace.dependencies.user_metadata_handlers] +path = "features/user_metadata/handlers" [workspace.dependencies.holoom_types] path = "crates/holoom_types" diff --git a/crates/hdk_utils/Cargo.toml b/crates/hdk_utils/Cargo.toml new file mode 100644 index 0000000..ea4211c --- /dev/null +++ b/crates/hdk_utils/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "hdk_utils" +version = "0.0.1" +edition = "2021" + +[lib] +crate-type = ["cdylib", "rlib"] +name = "hdk_utils" + +[dependencies] +hdk = { workspace = true } diff --git a/crates/hdk_utils/src/lib.rs b/crates/hdk_utils/src/lib.rs new file mode 100644 index 0000000..4a5417d --- /dev/null +++ b/crates/hdk_utils/src/lib.rs @@ -0,0 +1,25 @@ +use hdk::prelude::*; + +pub fn create_link( + base_address: impl Into, + target_address: impl Into, + link_type: impl TryInto, + tag: impl Into, +) -> ExternResult { + let ScopedLinkType { + zome_index, + zome_type: link_type, + } = link_type + .try_into() + .map_err(|_| wasm_error!("Isn't valid link type for this zome".to_string()))?; + HDK.with(|h| { + h.borrow().create_link(CreateLinkInput::new( + base_address.into(), + target_address.into(), + zome_index, + link_type, + tag.into(), + ChainTopOrdering::default(), + )) + }) +} diff --git a/crates/holoom_dna_tests/Cargo.toml b/crates/holoom_dna_tests/Cargo.toml index 3fe6d62..62b4b4d 100644 --- a/crates/holoom_dna_tests/Cargo.toml +++ b/crates/holoom_dna_tests/Cargo.toml @@ -10,6 +10,7 @@ name = "holoom_dna_tests" [dependencies] serde = { workspace = true } holoom_types = { workspace = true } +username_registry_coordinator = { workspace = true } hdk = { workspace = true, features = ["encoding", "test_utils"] } holochain = { workspace = true, default-features = false, features = [ "test_utils", diff --git a/crates/holoom_dna_tests/src/tests/username_registry/mod.rs b/crates/holoom_dna_tests/src/tests/username_registry/mod.rs index 6d00bb4..239007b 100644 --- a/crates/holoom_dna_tests/src/tests/username_registry/mod.rs +++ b/crates/holoom_dna_tests/src/tests/username_registry/mod.rs @@ -1,6 +1,5 @@ mod external_id_attestation; mod oracle; mod recipe; -mod user_metadata; mod username_attestation; mod wallet_attestation; diff --git a/crates/holoom_dna_tests/src/tests/username_registry/user_metadata.rs b/crates/holoom_dna_tests/src/tests/username_registry/user_metadata.rs deleted file mode 100644 index 2ade9a5..0000000 --- a/crates/holoom_dna_tests/src/tests/username_registry/user_metadata.rs +++ /dev/null @@ -1,63 +0,0 @@ -use std::collections::HashMap; - -use holochain::conductor::api::error::ConductorApiError; -use holoom_types::{GetMetadataItemValuePayload, UpdateMetadataItemPayload}; - -use crate::TestSetup; - -#[tokio::test(flavor = "multi_thread")] -async fn users_can_only_update_their_own_metadata() { - let setup = TestSetup::authority_and_alice_bob().await; - setup.conductors.exchange_peer_info().await; - - // Alice starts with no metadata - let initial_metadata: HashMap = setup - .alice_call("username_registry", "get_metadata", setup.alice_pubkey()) - .await - .unwrap(); - assert_eq!(initial_metadata, HashMap::default()); - - // Bob cannot set Alice's metadata - let res1: Result<(), ConductorApiError> = setup - .bob_call( - "username_registry", - "update_metadata_item", - UpdateMetadataItemPayload { - agent_pubkey: setup.alice_pubkey(), - name: "foo".into(), - value: "bar".into(), - }, - ) - .await; - assert!(res1.is_err()); - - // Alice sets an item - let _: () = setup - .alice_call( - "username_registry", - "update_metadata_item", - UpdateMetadataItemPayload { - agent_pubkey: setup.alice_pubkey(), - name: "foo".into(), - value: "bar2".into(), - }, - ) - .await - .unwrap(); - - setup.consistency().await; - - // Bob sees new item - let value1: String = setup - .bob_call( - "username_registry", - "get_metadata_item_value", - GetMetadataItemValuePayload { - agent_pubkey: setup.alice_pubkey(), - name: "foo".into(), - }, - ) - .await - .unwrap(); - assert_eq!(value1, String::from("bar2")); -} diff --git a/crates/holoom_types/src/lib.rs b/crates/holoom_types/src/lib.rs index a43508a..dbbfac2 100644 --- a/crates/holoom_types/src/lib.rs +++ b/crates/holoom_types/src/lib.rs @@ -6,9 +6,7 @@ use ts_rs::TS; pub mod external_id; pub use external_id::*; pub mod evm_signing_offer; -pub mod metadata; pub mod recipe; -pub use metadata::*; pub mod wallet; pub use wallet::*; pub mod username; diff --git a/crates/holoom_types/src/metadata.rs b/crates/holoom_types/src/metadata.rs deleted file mode 100644 index 1ee6e4b..0000000 --- a/crates/holoom_types/src/metadata.rs +++ /dev/null @@ -1,27 +0,0 @@ -use hdi::prelude::*; -use serde::{Deserialize, Serialize}; -use ts_rs::TS; - -#[derive(Serialize, Deserialize, Debug, TS)] -#[ts(export)] -pub struct MetadataItem { - pub name: String, - pub value: String, -} - -#[derive(Serialize, Deserialize, Debug, TS)] -#[ts(export)] -pub struct UpdateMetadataItemPayload { - #[ts(type = "AgentPubKey")] - pub agent_pubkey: AgentPubKey, - pub name: String, - pub value: String, -} - -#[derive(Serialize, Deserialize, Debug, TS)] -#[ts(export)] -pub struct GetMetadataItemValuePayload { - #[ts(type = "AgentPubKey")] - pub agent_pubkey: AgentPubKey, - pub name: String, -} diff --git a/crates/records_coordinator/Cargo.toml b/crates/records_coordinator/Cargo.toml index d650c9a..0df9350 100644 --- a/crates/records_coordinator/Cargo.toml +++ b/crates/records_coordinator/Cargo.toml @@ -10,3 +10,4 @@ name = "records_coordinator" [dependencies] hdk = { workspace = true } serde = { workspace = true } +typeshare = { workspace = true } diff --git a/crates/records_coordinator/src/lib.rs b/crates/records_coordinator/src/lib.rs index 9613140..4681e68 100644 --- a/crates/records_coordinator/src/lib.rs +++ b/crates/records_coordinator/src/lib.rs @@ -1,10 +1,94 @@ use hdk::prelude::*; +use typeshare::typeshare; #[hdk_extern] pub fn get_record(action_hash: ActionHash) -> ExternResult> { get(action_hash, GetOptions::network()) } +/// Input to the `create_app_entry_raw` function +#[derive(Serialize, Deserialize, Debug)] +#[typeshare] +pub struct CreateAppEntryRawInput { + /// The index of the zome that defines the entry type + pub zome_index: ZomeIndex, + /// The index of the entry definition within the zome + pub entry_def_index: EntryDefIndex, + /// The msgpack serialised app entry content + pub entry_bytes: SerializedBytes, +} + +/// Directly exposes the hdk functionality for creating entries. This is useful for validation +/// logic tests, because it removes the need for entry coordinator zomes to define create_* methods +/// for unintended actions, merely for the sake of asserting fail cases. +/// +/// For now, assumes all entries are public +#[hdk_extern] +pub fn create_app_entry_raw(input: CreateAppEntryRawInput) -> ExternResult { + let create_input = CreateInput { + entry_location: EntryDefLocation::App(AppEntryDefLocation { + zome_index: input.zome_index, + entry_def_index: input.entry_def_index, + }), + entry: Entry::App(AppEntryBytes(input.entry_bytes)), + entry_visibility: EntryVisibility::default(), + chain_top_ordering: ChainTopOrdering::default(), + }; + create(create_input) +} + +/// Directly exposes the hdk functionality for deleting entries. This is useful for validation +/// logic tests, because it removes the need for entry coordinator zomes to define delete_* methods +/// for unintended actions, merely for the sake of asserting fail cases. +#[hdk_extern] +pub fn delete_entry_raw(action_hash: ActionHash) -> ExternResult { + let delete_input = DeleteInput { + deletes_action_hash: action_hash, + chain_top_ordering: ChainTopOrdering::default(), + }; + delete(delete_input) +} + +/// Input to the `create_link_raw` function +#[derive(Serialize, Deserialize, Debug)] +#[typeshare] +pub struct CreateLinkRawInput { + /// The 'form' address of the link + base_address: AnyLinkableHash, + /// The 'to' address of the link + target_address: AnyLinkableHash, + /// The index of the zome in which the link was defined + zome_index: ZomeIndex, + /// The index of the link definition within the zome + link_type: LinkType, + /// Freeform data attached to the link + tag: LinkTag, +} + +/// Directly exposes the hdk functionality for creating links. This is useful for validation logic +/// tests, because it removes the need for entry coordinator zomes to define create_* methods for +/// unintended actions, merely for the sake of asserting fail cases. +#[hdk_extern] +pub fn create_link_raw(input: CreateLinkRawInput) -> ExternResult { + let input = CreateLinkInput { + base_address: input.base_address, + target_address: input.target_address, + zome_index: input.zome_index, + link_type: input.link_type, + tag: input.tag, + chain_top_ordering: ChainTopOrdering::default(), + }; + HDK.with(|h| h.borrow().create_link(input)) +} + +/// Directly exposes the hdk functionality for deleting links. This is useful for validation logic +/// tests, because it removes the need for entry coordinator zomes to define delete_* methods for +/// unintended actions, merely for the sake of asserting fail cases. +#[hdk_extern] +pub fn delete_link_raw(create_link_action_hash: ActionHash) -> ExternResult { + delete_link(create_link_action_hash) +} + #[hdk_extern] pub fn get_chain_status(agent: AgentPubKey) -> ExternResult { get_agent_activity(agent, ChainQueryFilter::default(), ActivityRequest::Status) diff --git a/crates/username_registry_coordinator/Cargo.toml b/crates/username_registry_coordinator/Cargo.toml index 4bcfb8c..53bb825 100644 --- a/crates/username_registry_coordinator/Cargo.toml +++ b/crates/username_registry_coordinator/Cargo.toml @@ -10,12 +10,14 @@ name = "username_registry_coordinator" [dependencies] serde = { workspace = true } hdk = { workspace = true } -bincode = { workspace = true } username_registry_integrity = { workspace = true } holoom_types = { workspace = true } username_registry_validation = { workspace = true } username_registry_utils = { workspace = true } +user_metadata_types = { workspace = true } +user_metadata_handlers = { workspace = true } jaq_wrapper = { workspace = true } indexmap = "2.2.6" serde_json = { workspace = true } hex = { workspace = true } +typeshare = { workspace = true } diff --git a/crates/username_registry_coordinator/src/user_metadata.rs b/crates/username_registry_coordinator/src/user_metadata.rs index e66e53d..043b646 100644 --- a/crates/username_registry_coordinator/src/user_metadata.rs +++ b/crates/username_registry_coordinator/src/user_metadata.rs @@ -1,77 +1,50 @@ use std::collections::HashMap; +use typeshare::typeshare; use hdk::prelude::*; -use holoom_types::{GetMetadataItemValuePayload, MetadataItem, UpdateMetadataItemPayload}; -use username_registry_integrity::*; +use user_metadata_types::MetadataItem; +use username_registry_integrity::LinkTypes; +/// The input argument to `update_metadata_item`` +#[typeshare] +#[derive(Serialize, Deserialize, Debug)] +pub struct UpdateMetadataItemInput { + /// The key for the particular metadata item + pub name: String, + /// The value to assign to the key + pub value: String, +} + +/// Upsert a key-value item of your own metadata #[hdk_extern] -pub fn update_metadata_item(payload: UpdateMetadataItemPayload) -> ExternResult<()> { - let links = get_links( - GetLinksInputBuilder::try_new(payload.agent_pubkey.clone(), LinkTypes::AgentMetadata)? - .build(), - )?; - for link in links { - let existing_item: MetadataItem = - bincode::deserialize(&link.tag.into_inner()).map_err(|_| { - wasm_error!(WasmErrorInner::Guest( - "Failed to deserialize MetadataItem".into() - )) - })?; - if existing_item.name == payload.name { - // Remove old MetadataItem - delete_link(link.create_link_hash)?; - } - } - let item = MetadataItem { - name: payload.name, - value: payload.value, - }; - let tag_bytes = bincode::serialize(&item).map_err(|_| { - wasm_error!(WasmErrorInner::Guest( - "Failed to serialize MetadataItem".into() - )) - })?; - create_link( - payload.agent_pubkey.clone(), - payload.agent_pubkey, // unused and irrelevant - LinkTypes::AgentMetadata, - LinkTag(tag_bytes), - )?; - Ok(()) +pub fn update_metadata_item(input: UpdateMetadataItemInput) -> ExternResult<()> { + user_metadata_handlers::update_item::handler::(MetadataItem { + name: input.name, + value: input.value, + }) +} + +/// The input argument to `get_metadata_item_value`` +#[typeshare] +#[derive(Serialize, Deserialize, Debug)] +pub struct GetMetadataItemValueInput { + /// The agent whose metadata you wish to query + pub agent_pubkey: AgentPubKey, + /// The key for the particular metadata item + pub name: String, } +/// Retrieve a particular metadata value for a given key on an agent. +/// +/// Return `None` if the key-pair isn't found, which means the user hasn't set it, or the +/// information hasn't been gossiped. #[hdk_extern] -pub fn get_metadata_item_value( - payload: GetMetadataItemValuePayload, -) -> ExternResult> { - let links = get_links( - GetLinksInputBuilder::try_new(payload.agent_pubkey, LinkTypes::AgentMetadata)?.build(), - )?; - for link in links { - let item: MetadataItem = bincode::deserialize(&link.tag.into_inner()).map_err(|_| { - wasm_error!(WasmErrorInner::Guest( - "Failed to deserialize MetadataItem".into() - )) - })?; - if payload.name == item.name { - return Ok(Some(item.value)); - } - } - Ok(None) +pub fn get_metadata_item_value(input: GetMetadataItemValueInput) -> ExternResult> { + user_metadata_handlers::get_item::handler::(input.agent_pubkey, input.name) } +/// Retrieves all metadata known for the specified user as key-value map #[hdk_extern] pub fn get_metadata(agent_pubkey: AgentPubKey) -> ExternResult> { - let links = - get_links(GetLinksInputBuilder::try_new(agent_pubkey, LinkTypes::AgentMetadata)?.build())?; - let mut out = HashMap::default(); - for link in links { - let item: MetadataItem = bincode::deserialize(&link.tag.into_inner()).map_err(|_| { - wasm_error!(WasmErrorInner::Guest( - "Failed to deserialize MetadataItem".into() - )) - })?; - out.insert(item.name, item.value); - } - Ok(out) + user_metadata_handlers::get_all::handler::(agent_pubkey) } diff --git a/crates/username_registry_integrity/Cargo.toml b/crates/username_registry_integrity/Cargo.toml index a0f9d12..a580ad8 100644 --- a/crates/username_registry_integrity/Cargo.toml +++ b/crates/username_registry_integrity/Cargo.toml @@ -10,5 +10,9 @@ name = "username_registry_integrity" [dependencies] hdi = { workspace = true } serde = { workspace = true } +serde_json = { workspace = true } holoom_types = { workspace = true } username_registry_validation = { workspace = true } +user_metadata_types = { workspace = true } +user_metadata_validation = { workspace = true } +typeshare = { workspace = true } diff --git a/crates/username_registry_integrity/src/lib.rs b/crates/username_registry_integrity/src/lib.rs index a3f2fa1..bf52e2d 100644 --- a/crates/username_registry_integrity/src/lib.rs +++ b/crates/username_registry_integrity/src/lib.rs @@ -2,6 +2,7 @@ use hdi::prelude::*; pub mod entry_types; pub use entry_types::*; pub mod link_types; +pub mod rejection_detail; pub use link_types::*; #[hdk_extern] diff --git a/crates/username_registry_integrity/src/link_types.rs b/crates/username_registry_integrity/src/link_types.rs index efe895f..1ffda54 100644 --- a/crates/username_registry_integrity/src/link_types.rs +++ b/crates/username_registry_integrity/src/link_types.rs @@ -1,6 +1,9 @@ use hdi::prelude::*; +use user_metadata_types::InjectMetadataLinkTypes; use username_registry_validation::*; +use crate::rejection_detail::ValidationRejectionDetail; + #[derive(Serialize, Deserialize)] #[hdk_link_types] pub enum LinkTypes { @@ -16,6 +19,13 @@ pub enum LinkTypes { EvmAddressToSigningOffer, } +impl InjectMetadataLinkTypes for LinkTypes { + type LinkType = LinkTypes; + fn agent_metadata() -> Self::LinkType { + Self::AgentMetadata + } +} + impl LinkTypes { pub fn validate_create( self, @@ -34,7 +44,16 @@ impl LinkTypes { ) } LinkTypes::AgentMetadata => { - validate_create_link_user_metadata(action, base_address, target_address, tag) + ValidationRejectionDetail::CreateAgentMetadataLinkRejectionReasons( + user_metadata_validation::validate_create_link_user_metadata( + action, + base_address, + target_address, + tag, + )? + .into(), + ) + .to_validation_result() } LinkTypes::AgentToWalletAttestations => { validate_create_link_agent_to_wallet_attestations( @@ -110,13 +129,19 @@ impl LinkTypes { tag, ) } - LinkTypes::AgentMetadata => validate_delete_link_user_metadata( - action, - original_action, - base_address, - target_address, - tag, - ), + LinkTypes::AgentMetadata => { + ValidationRejectionDetail::DeleteAgentMetadataLinkRejectionReasons( + user_metadata_validation::validate_delete_link_user_metadata( + action, + original_action, + base_address, + target_address, + tag, + )? + .into(), + ) + .to_validation_result() + } LinkTypes::AgentToWalletAttestations => { validate_delete_link_agent_to_wallet_attestations( action, diff --git a/crates/username_registry_integrity/src/rejection_detail.rs b/crates/username_registry_integrity/src/rejection_detail.rs new file mode 100644 index 0000000..cdbc233 --- /dev/null +++ b/crates/username_registry_integrity/src/rejection_detail.rs @@ -0,0 +1,35 @@ +use hdi::prelude::*; +use typeshare::typeshare; +use user_metadata_validation::{ + CreateAgentMetadataLinkRejectionReason, DeleteAgentMetadataLinkRejectionReason, +}; + +#[derive(Serialize)] +#[serde(tag = "type", content = "reasons")] +#[typeshare] +pub enum ValidationRejectionDetail { + CreateAgentMetadataLinkRejectionReasons(Vec), + DeleteAgentMetadataLinkRejectionReasons(Vec), +} + +impl ValidationRejectionDetail { + pub fn to_validation_result(self) -> ExternResult { + let is_valid = match &self { + ValidationRejectionDetail::CreateAgentMetadataLinkRejectionReasons(reasons) => { + reasons.is_empty() + } + ValidationRejectionDetail::DeleteAgentMetadataLinkRejectionReasons(reasons) => { + reasons.is_empty() + } + }; + if is_valid { + Ok(ValidateCallbackResult::Valid) + } else { + Ok(ValidateCallbackResult::Invalid(format!( + "__REASONS_START__{}__REASONS_END__", + serde_json::to_string(&self) + .map_err(|_| wasm_error!("Couldn't serialize rejection reasons".to_string()))? + ))) + } + } +} diff --git a/crates/username_registry_utils/Cargo.toml b/crates/username_registry_utils/Cargo.toml index 756ca7a..7778cef 100644 --- a/crates/username_registry_utils/Cargo.toml +++ b/crates/username_registry_utils/Cargo.toml @@ -11,7 +11,6 @@ name = "username_registry_utils" hdi = { workspace = true } holo_hash = { workspace = true } serde = { workspace = true } -bincode = { workspace = true } holoom_types = { workspace = true } ed25519-dalek = { workspace = true } bs58 = { workspace = true } diff --git a/crates/username_registry_validation/Cargo.toml b/crates/username_registry_validation/Cargo.toml index 87c0252..efaea2e 100644 --- a/crates/username_registry_validation/Cargo.toml +++ b/crates/username_registry_validation/Cargo.toml @@ -12,7 +12,6 @@ hdi = { workspace = true } holo_hash = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } -bincode = { workspace = true } holoom_types = { workspace = true } ed25519-dalek = { workspace = true } bs58 = { workspace = true } diff --git a/crates/username_registry_validation/src/lib.rs b/crates/username_registry_validation/src/lib.rs index a39669d..062d6ad 100644 --- a/crates/username_registry_validation/src/lib.rs +++ b/crates/username_registry_validation/src/lib.rs @@ -2,8 +2,6 @@ pub mod username_attestation; pub use username_attestation::*; pub mod wallet_attestation; pub use wallet_attestation::*; -pub mod user_metadata; -pub use user_metadata::*; pub mod oracle_document; pub use oracle_document::*; pub mod agent_username_attestation; diff --git a/crates/username_registry_validation/src/user_metadata.rs b/crates/username_registry_validation/src/user_metadata.rs deleted file mode 100644 index 06ef711..0000000 --- a/crates/username_registry_validation/src/user_metadata.rs +++ /dev/null @@ -1,44 +0,0 @@ -use hdi::prelude::*; -use holoom_types::MetadataItem; - -pub fn validate_create_link_user_metadata( - action: CreateLink, - base_address: AnyLinkableHash, - _target_address: AnyLinkableHash, - tag: LinkTag, -) -> ExternResult { - let agent_pubkey = AgentPubKey::try_from(base_address).map_err(|e| wasm_error!(e))?; - - if action.author != agent_pubkey { - return Ok(ValidateCallbackResult::Invalid( - "Only the owner can embed metadata in their link tags".into(), - )); - } - // The contents of the target_address is unused and irrelevant - - // Check the tag is valid - let _item: MetadataItem = bincode::deserialize(&tag.into_inner()).map_err(|_| { - wasm_error!(WasmErrorInner::Guest( - "Failed to deserialize MetadataItem".into() - )) - })?; - - Ok(ValidateCallbackResult::Valid) -} -pub fn validate_delete_link_user_metadata( - action: DeleteLink, - _original_action: CreateLink, - base_address: AnyLinkableHash, - _target_address: AnyLinkableHash, - _tag: LinkTag, -) -> ExternResult { - let agent_pubkey = AgentPubKey::try_from(base_address).map_err(|e| wasm_error!(e))?; - - if action.author != agent_pubkey { - return Ok(ValidateCallbackResult::Invalid( - "Only the owner can delete their metadata tags".into(), - )); - } - - Ok(ValidateCallbackResult::Valid) -} diff --git a/features/user_metadata/handlers/Cargo.toml b/features/user_metadata/handlers/Cargo.toml new file mode 100644 index 0000000..f664fab --- /dev/null +++ b/features/user_metadata/handlers/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "user_metadata_handlers" +version = "0.0.1" +edition = "2021" + +[lib] +crate-type = ["cdylib", "rlib"] +name = "user_metadata_handlers" + +[dependencies] +serde = { workspace = true } +hdk = { workspace = true } +user_metadata_types = { workspace = true } +hdk_utils = { workspace = true } diff --git a/features/user_metadata/handlers/src/get_all.rs b/features/user_metadata/handlers/src/get_all.rs new file mode 100644 index 0000000..88bc858 --- /dev/null +++ b/features/user_metadata/handlers/src/get_all.rs @@ -0,0 +1,22 @@ +use std::collections::HashMap; + +use hdk::prelude::*; +use user_metadata_types::{InjectMetadataLinkTypes, MetadataItem}; + +pub fn handler(agent_pubkey: AgentPubKey) -> ExternResult> +where + LT: InjectMetadataLinkTypes, +{ + let links = + get_links(GetLinksInputBuilder::try_new(agent_pubkey, LT::agent_metadata())?.build())?; + let mut out = HashMap::default(); + for link in links { + let item: MetadataItem = ExternIO(link.tag.into_inner()).decode().map_err(|_| { + wasm_error!(WasmErrorInner::Guest( + "Failed to deserialize MetadataItem".into() + )) + })?; + out.insert(item.name, item.value); + } + Ok(out) +} diff --git a/features/user_metadata/handlers/src/get_item.rs b/features/user_metadata/handlers/src/get_item.rs new file mode 100644 index 0000000..eab09dc --- /dev/null +++ b/features/user_metadata/handlers/src/get_item.rs @@ -0,0 +1,21 @@ +use hdk::prelude::*; +use user_metadata_types::{InjectMetadataLinkTypes, MetadataItem}; + +pub fn handler(agent_pubkey: AgentPubKey, name: String) -> ExternResult> +where + LT: InjectMetadataLinkTypes, +{ + let links = + get_links(GetLinksInputBuilder::try_new(agent_pubkey, LT::agent_metadata())?.build())?; + for link in links { + let item: MetadataItem = ExternIO(link.tag.into_inner()).decode().map_err(|_| { + wasm_error!(WasmErrorInner::Guest( + "Failed to deserialize MetadataItem".into() + )) + })?; + if name == item.name { + return Ok(Some(item.value)); + } + } + Ok(None) +} diff --git a/features/user_metadata/handlers/src/lib.rs b/features/user_metadata/handlers/src/lib.rs new file mode 100644 index 0000000..de3f767 --- /dev/null +++ b/features/user_metadata/handlers/src/lib.rs @@ -0,0 +1,3 @@ +pub mod get_all; +pub mod get_item; +pub mod update_item; diff --git a/features/user_metadata/handlers/src/update_item.rs b/features/user_metadata/handlers/src/update_item.rs new file mode 100644 index 0000000..755d261 --- /dev/null +++ b/features/user_metadata/handlers/src/update_item.rs @@ -0,0 +1,36 @@ +use hdk::prelude::*; +use user_metadata_types::{InjectMetadataLinkTypes, MetadataItem}; + +pub fn handler(item: MetadataItem) -> ExternResult<()> +where + LT: InjectMetadataLinkTypes, +{ + let agent_pubkey = agent_info()?.agent_initial_pubkey; + let links = get_links( + GetLinksInputBuilder::try_new(agent_pubkey.clone(), LT::agent_metadata())?.build(), + )?; + for link in links { + let existing_item: MetadataItem = + ExternIO(link.tag.into_inner()).decode().map_err(|_| { + wasm_error!(WasmErrorInner::Guest( + "Failed to deserialize MetadataItem".into() + )) + })?; + if existing_item.name == item.name { + // Remove old MetadataItem + delete_link(link.create_link_hash)?; + } + } + let tag_bytes = ExternIO::encode(item).map_err(|_| { + wasm_error!(WasmErrorInner::Guest( + "Failed to serialize MetadataItem".into() + )) + })?; + hdk_utils::create_link( + agent_pubkey.clone(), + agent_pubkey, // unused and irrelevant + LT::agent_metadata(), + LinkTag(tag_bytes.into_vec()), + )?; + Ok(()) +} diff --git a/features/user_metadata/types/Cargo.toml b/features/user_metadata/types/Cargo.toml new file mode 100644 index 0000000..5af54af --- /dev/null +++ b/features/user_metadata/types/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "user_metadata_types" +version = "0.0.1" +edition = "2021" + +[lib] +crate-type = ["cdylib", "rlib"] +name = "user_metadata_types" + +[dependencies] +serde = { workspace = true } +hdi = { workspace = true } +typeshare = { workspace = true } diff --git a/features/user_metadata/types/src/lib.rs b/features/user_metadata/types/src/lib.rs new file mode 100644 index 0000000..20a2fc2 --- /dev/null +++ b/features/user_metadata/types/src/lib.rs @@ -0,0 +1,18 @@ +use hdi::{link::LinkTypeFilterExt, prelude::ScopedLinkType}; +use serde::{Deserialize, Serialize}; +use typeshare::typeshare; + +/// A key-value pair, representing an item of a user's metadata +#[typeshare] +#[derive(Serialize, Deserialize, Debug)] +pub struct MetadataItem { + pub name: String, + pub value: String, +} + +/// A helper trait that allows the coordinator zome to inject the corresponding integrity zome's +/// relevant `LinkTypes` information into this crate, avoid a circular dependency. +pub trait InjectMetadataLinkTypes { + type LinkType: LinkTypeFilterExt + TryInto; + fn agent_metadata() -> Self::LinkType; +} diff --git a/features/user_metadata/validation/Cargo.toml b/features/user_metadata/validation/Cargo.toml new file mode 100644 index 0000000..72e1c99 --- /dev/null +++ b/features/user_metadata/validation/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "user_metadata_validation" +version = "0.0.1" +edition = "2021" + +[lib] +crate-type = ["cdylib", "rlib"] +name = "user_metadata_validation" + +[dependencies] +hdi = { workspace = true } +user_metadata_types = { workspace = true } +serde = { workspace = true } +typeshare = { workspace = true } diff --git a/features/user_metadata/validation/src/lib.rs b/features/user_metadata/validation/src/lib.rs new file mode 100644 index 0000000..3a23937 --- /dev/null +++ b/features/user_metadata/validation/src/lib.rs @@ -0,0 +1,68 @@ +use hdi::prelude::*; +use serde::{Deserialize, Serialize}; +use typeshare::typeshare; +use user_metadata_types::MetadataItem; + +/// Reasons for which a create `AgentMetadata` link action can fail validation. +#[derive(Serialize, Deserialize)] +#[typeshare] +pub enum CreateAgentMetadataLinkRejectionReason { + /// The base address is the agent pubkey of the user who is being annotated with metadata. + /// As a user can only author their own metadata, the base address has match their own pubkey. + BaseAddressMustBeOwner, + + /// The link tag content doesn't match the expected key-value schema struct `MetadataItem`. + BadTagSerialization, +} + +/// Gathers any reasons for rejecting a create `AgentMetadata` link action +pub fn validate_create_link_user_metadata( + action: CreateLink, + base_address: AnyLinkableHash, + _target_address: AnyLinkableHash, + tag: LinkTag, +) -> ExternResult> { + use CreateAgentMetadataLinkRejectionReason::*; + let mut rejection_reasons = Vec::new(); + + if AnyLinkableHash::from(action.author) != base_address { + rejection_reasons.push(BaseAddressMustBeOwner); + } + + // The contents of the target_address is unused and irrelevant + + // Check the tag is valid + + if ExternIO(tag.into_inner()).decode::().is_err() { + rejection_reasons.push(BadTagSerialization) + } + + Ok(rejection_reasons) +} + +/// Reasons for which a delete `AgentMetadata` link action can fail validation. +#[derive(Serialize)] +#[typeshare] +pub enum DeleteAgentMetadataLinkRejectionReason { + /// The user attempting to delete the metadata item is not the owner and therefore doesn't + /// have permission. + DeleterIsNotOwner, +} + +/// Gathers any reasons for rejecting a delete `AgentMetadata`` link action +pub fn validate_delete_link_user_metadata( + action: DeleteLink, + _original_action: CreateLink, + base_address: AnyLinkableHash, + _target_address: AnyLinkableHash, + _tag: LinkTag, +) -> ExternResult> { + use DeleteAgentMetadataLinkRejectionReason::*; + let mut rejection_reasons = Vec::new(); + + if AnyLinkableHash::from(action.author) != base_address { + rejection_reasons.push(DeleterIsNotOwner) + } + + Ok(rejection_reasons) +} diff --git a/flake.nix b/flake.nix index 16fa305..b741d16 100644 --- a/flake.nix +++ b/flake.nix @@ -21,6 +21,7 @@ # add further packages from nixpkgs nodejs cargo-nextest + typeshare ]; }; }; diff --git a/package-lock.json b/package-lock.json index ae0010a..f89f35b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -944,6 +944,22 @@ "node": ">=12" } }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.0.tgz", + "integrity": "sha512-suXjq53gERueVWu0OKxzWqk7NxiUWSUlrxoZK7usiF50C6ipColGR5qie2496iKGYNLhDZkPxBI3erbnYkU0rQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/openbsd-x64": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", @@ -5445,6 +5461,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/get-tsconfig": { + "version": "4.7.6", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.7.6.tgz", + "integrity": "sha512-ZAqrLlu18NbDdRaHq+AKXzAmqIUPswPWKUchfytdAjiRFnCe5ojG2bstg6mRiZabkKfCoL/e98pbBELIV/YCeA==", + "dev": true, + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/get-uri": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.3.tgz", @@ -8580,6 +8608,15 @@ "node": ">=8" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/resolve.exports": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", @@ -9729,6 +9766,432 @@ "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", "dev": true }, + "node_modules/tsx": { + "version": "4.17.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.17.0.tgz", + "integrity": "sha512-eN4mnDA5UMKDt4YZixo9tBioibaMBpoxBkD+rIPAjVmYERSG0/dWEY1CEFuV89CgASlKL499q8AhmkMnnjtOJg==", + "dev": true, + "dependencies": { + "esbuild": "~0.23.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tsx/node_modules/@esbuild/aix-ppc64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.0.tgz", + "integrity": "sha512-3sG8Zwa5fMcA9bgqB8AfWPQ+HFke6uD3h1s3RIwUNK8EG7a4buxvuFTs3j1IMs2NXAk9F30C/FF4vxRgQCcmoQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.0.tgz", + "integrity": "sha512-+KuOHTKKyIKgEEqKbGTK8W7mPp+hKinbMBeEnNzjJGyFcWsfrXjSTNluJHCY1RqhxFurdD8uNXQDei7qDlR6+g==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.0.tgz", + "integrity": "sha512-EuHFUYkAVfU4qBdyivULuu03FhJO4IJN9PGuABGrFy4vUuzk91P2d+npxHcFdpUnfYKy0PuV+n6bKIpHOB3prQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-x64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.0.tgz", + "integrity": "sha512-WRrmKidLoKDl56LsbBMhzTTBxrsVwTKdNbKDalbEZr0tcsBgCLbEtoNthOW6PX942YiYq8HzEnb4yWQMLQuipQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-arm64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.0.tgz", + "integrity": "sha512-YLntie/IdS31H54Ogdn+v50NuoWF5BDkEUFpiOChVa9UnKpftgwzZRrI4J132ETIi+D8n6xh9IviFV3eXdxfow==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-x64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.0.tgz", + "integrity": "sha512-IMQ6eme4AfznElesHUPDZ+teuGwoRmVuuixu7sv92ZkdQcPbsNHzutd+rAfaBKo8YK3IrBEi9SLLKWJdEvJniQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.0.tgz", + "integrity": "sha512-0muYWCng5vqaxobq6LB3YNtevDFSAZGlgtLoAc81PjUfiFz36n4KMpwhtAd4he8ToSI3TGyuhyx5xmiWNYZFyw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-x64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.0.tgz", + "integrity": "sha512-XKDVu8IsD0/q3foBzsXGt/KjD/yTKBCIwOHE1XwiXmrRwrX6Hbnd5Eqn/WvDekddK21tfszBSrE/WMaZh+1buQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.0.tgz", + "integrity": "sha512-SEELSTEtOFu5LPykzA395Mc+54RMg1EUgXP+iw2SJ72+ooMwVsgfuwXo5Fn0wXNgWZsTVHwY2cg4Vi/bOD88qw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.0.tgz", + "integrity": "sha512-j1t5iG8jE7BhonbsEg5d9qOYcVZv/Rv6tghaXM/Ug9xahM0nX/H2gfu6X6z11QRTMT6+aywOMA8TDkhPo8aCGw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ia32": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.0.tgz", + "integrity": "sha512-P7O5Tkh2NbgIm2R6x1zGJJsnacDzTFcRWZyTTMgFdVit6E98LTxO+v8LCCLWRvPrjdzXHx9FEOA8oAZPyApWUA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-loong64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.0.tgz", + "integrity": "sha512-InQwepswq6urikQiIC/kkx412fqUZudBO4SYKu0N+tGhXRWUqAx+Q+341tFV6QdBifpjYgUndV1hhMq3WeJi7A==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-mips64el": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.0.tgz", + "integrity": "sha512-J9rflLtqdYrxHv2FqXE2i1ELgNjT+JFURt/uDMoPQLcjWQA5wDKgQA4t/dTqGa88ZVECKaD0TctwsUfHbVoi4w==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ppc64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.0.tgz", + "integrity": "sha512-cShCXtEOVc5GxU0fM+dsFD10qZ5UpcQ8AM22bYj0u/yaAykWnqXJDpd77ublcX6vdDsWLuweeuSNZk4yUxZwtw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-riscv64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.0.tgz", + "integrity": "sha512-HEtaN7Y5UB4tZPeQmgz/UhzoEyYftbMXrBCUjINGjh3uil+rB/QzzpMshz3cNUxqXN7Vr93zzVtpIDL99t9aRw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-s390x": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.0.tgz", + "integrity": "sha512-WDi3+NVAuyjg/Wxi+o5KPqRbZY0QhI9TjrEEm+8dmpY9Xir8+HE/HNx2JoLckhKbFopW0RdO2D72w8trZOV+Wg==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-x64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.0.tgz", + "integrity": "sha512-a3pMQhUEJkITgAw6e0bWA+F+vFtCciMjW/LPtoj99MhVt+Mfb6bbL9hu2wmTZgNd994qTAEw+U/r6k3qHWWaOQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/netbsd-x64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.0.tgz", + "integrity": "sha512-cRK+YDem7lFTs2Q5nEv/HHc4LnrfBCbH5+JHu6wm2eP+d8OZNoSMYgPZJq78vqQ9g+9+nMuIsAO7skzphRXHyw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openbsd-x64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.0.tgz", + "integrity": "sha512-6p3nHpby0DM/v15IFKMjAaayFhqnXV52aEmv1whZHX56pdkK+MEaLoQWj+H42ssFarP1PcomVhbsR4pkz09qBg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/sunos-x64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.0.tgz", + "integrity": "sha512-BFelBGfrBwk6LVrmFzCq1u1dZbG4zy/Kp93w2+y83Q5UGYF1d8sCzeLI9NXjKyujjBBniQa8R8PzLFAUrSM9OA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-arm64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.0.tgz", + "integrity": "sha512-lY6AC8p4Cnb7xYHuIxQ6iYPe6MfO2CC43XXKo9nBXDb35krYt7KGhQnOkRGar5psxYkircpCqfbNDB4uJbS2jQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-ia32": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.0.tgz", + "integrity": "sha512-7L1bHlOTcO4ByvI7OXVI5pNN6HSu6pUQq9yodga8izeuB1KcT2UkHaH6118QJwopExPn0rMHIseCTx1CRo/uNA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-x64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.0.tgz", + "integrity": "sha512-Arm+WgUFLUATuoxCJcahGuk6Yj9Pzxd6l11Zb/2aAuv5kWWvvfhLFo2fni4uSK5vzlUdCGZ/BdV5tH8klj8p8g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/esbuild": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.0.tgz", + "integrity": "sha512-1lvV17H2bMYda/WaFb2jLPeHU3zml2k4/yagNMG8Q/YtfMjCwEUZa2eXXMgZTVSL5q1n4H7sQ0X6CdJDqqeCFA==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.23.0", + "@esbuild/android-arm": "0.23.0", + "@esbuild/android-arm64": "0.23.0", + "@esbuild/android-x64": "0.23.0", + "@esbuild/darwin-arm64": "0.23.0", + "@esbuild/darwin-x64": "0.23.0", + "@esbuild/freebsd-arm64": "0.23.0", + "@esbuild/freebsd-x64": "0.23.0", + "@esbuild/linux-arm": "0.23.0", + "@esbuild/linux-arm64": "0.23.0", + "@esbuild/linux-ia32": "0.23.0", + "@esbuild/linux-loong64": "0.23.0", + "@esbuild/linux-mips64el": "0.23.0", + "@esbuild/linux-ppc64": "0.23.0", + "@esbuild/linux-riscv64": "0.23.0", + "@esbuild/linux-s390x": "0.23.0", + "@esbuild/linux-x64": "0.23.0", + "@esbuild/netbsd-x64": "0.23.0", + "@esbuild/openbsd-arm64": "0.23.0", + "@esbuild/openbsd-x64": "0.23.0", + "@esbuild/sunos-x64": "0.23.0", + "@esbuild/win32-arm64": "0.23.0", + "@esbuild/win32-ia32": "0.23.0", + "@esbuild/win32-x64": "0.23.0" + } + }, "node_modules/tweetnacl": { "version": "0.14.5", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", @@ -11051,7 +11514,9 @@ "prettier": "^3.3.3", "rimraf": "^5.0.5", "rollup": "^4.12.0", - "rollup-plugin-cleanup": "^3.2.1" + "rollup-plugin-cleanup": "^3.2.1", + "tsx": "^4.17.0", + "yaml": "^2.5.0" }, "peerDependencies": { "@holochain/client": "^0.18.0-dev" diff --git a/package.json b/package.json index a7502a5..043e910 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ }, "scripts": { "build:dna": "scripts/build_dna.sh", - "test:tryorama": "npm run build:dna && npm run -w @holoom/types build && npm t -w @holoom/tryorama", + "test:tryorama": "npm run build:dna && npm run prepare-build:types && npm t -w @holoom/tryorama", "test:dna": "npm run build:dna && cargo nextest run -j 1", "prepare-build:types": "rimraf crates/holoom_types/bindings && cargo test --package holoom_types && npm run -w @holoom/types prepare:bindings && npm run -w @holoom/types build", "build:client": "npm run build -w @holoom/client", diff --git a/packages/client/src/holoom-client.ts b/packages/client/src/holoom-client.ts index 2d2a48f..05276f2 100644 --- a/packages/client/src/holoom-client.ts +++ b/packages/client/src/holoom-client.ts @@ -95,7 +95,6 @@ export class HoloomClient { */ async setMetadata(name: string, value: string) { await this.usernameRegistryCoordinator.updateMetadataItem({ - agent_pubkey: this.myPubKey, name, value, }); diff --git a/packages/tryorama/package.json b/packages/tryorama/package.json index 9718fdc..6ddbfd0 100644 --- a/packages/tryorama/package.json +++ b/packages/tryorama/package.json @@ -2,7 +2,7 @@ "name": "@holoom/tryorama", "private": true, "scripts": { - "test": "TRYORAMA_LOG_LEVEL=error vitest run" + "test": "TRYORAMA_LOG_LEVEL=debug vitest run" }, "dependencies": { "@holochain/client": "^0.18.0-dev.8", diff --git a/packages/tryorama/src/external_attestation/only_authority.test.ts b/packages/tryorama/src/external_attestation/only_authority.test.ts index c83dd41..3e2ad10 100644 --- a/packages/tryorama/src/external_attestation/only_authority.test.ts +++ b/packages/tryorama/src/external_attestation/only_authority.test.ts @@ -9,18 +9,11 @@ test("Only authority can create ExternalIdAttestations", async () => { const { authority, appBundleSource } = await setupBundleAndAuthorityPlayer(scenario); const alice = await scenario.addPlayerWithApp(appBundleSource); - + await scenario.shareAllAgents(); const authorityCoordinators = bindCoordinators(authority); const aliceCoordinators = bindCoordinators(alice); - // Shortcut peer discovery through gossip and register all agents in every - // conductor of the scenario. - await scenario.shareAllAgents(); - //--------------------------------------------------------------- - console.log( - "\n************************* START TEST ****************************\n" - ); - + // Alice cannot create External ID Attestation await expect( aliceCoordinators.usernameRegistry.createExternalIdAttestation({ request_id: "3563", @@ -33,8 +26,8 @@ test("Only authority can create ExternalIdAttestations", async () => { "Only the Username Registry Authority can create external ID attestations" ) ); - console.log("Checked Alice cannot create external ID attestation"); + // Authority can create External ID Attestation await expect( authorityCoordinators.usernameRegistry.createExternalIdAttestation({ request_id: "3563", @@ -43,6 +36,5 @@ test("Only authority can create ExternalIdAttestations", async () => { display_name: "Alice", }) ).resolves.toBeTruthy(); - console.log("Checked authority can create external ID attestation"); }); }); diff --git a/packages/tryorama/src/signer/sign.test.ts b/packages/tryorama/src/signer/sign.test.ts index d144f24..54620ac 100644 --- a/packages/tryorama/src/signer/sign.test.ts +++ b/packages/tryorama/src/signer/sign.test.ts @@ -3,7 +3,7 @@ import { runScenario } from "@holochain/tryorama"; import { overrideHappBundle } from "../utils/setup-happ.js"; import { bindCoordinators } from "../utils/bindings.js"; -import { fakeAgentPubKey } from "@holochain/client"; +import { fakeAgentPubKey, sliceCore32 } from "@holochain/client"; import { sha512 } from "@noble/hashes/sha512"; import * as ed from "@noble/ed25519"; import { encode } from "@msgpack/msgpack"; @@ -26,7 +26,7 @@ test("Sign message and verify signature", async () => { ed.verifyAsync( signature, encode(message), // signed as serialised :-/ - alice.agentPubKey.slice(3, 35) // get_raw_32() + sliceCore32(alice.agentPubKey) ) ).resolves.toBe(true); }); diff --git a/packages/tryorama/src/user_metadata/raw.test.ts b/packages/tryorama/src/user_metadata/raw.test.ts new file mode 100644 index 0000000..e4f1194 --- /dev/null +++ b/packages/tryorama/src/user_metadata/raw.test.ts @@ -0,0 +1,107 @@ +import { expect, test } from "vitest"; +import { runScenario } from "@holochain/tryorama"; + +import { overrideHappBundle } from "../utils/setup-happ.js"; +import { bindCoordinators } from "../utils/bindings.js"; +import { + fakeAgentPubKey, + hashFrom32AndType, + HoloHash, + sliceCore32, +} from "@holochain/client"; +import { + ValidationRejectionDetail, + CreateAgentMetadataLinkRejectionReason, + ValidationError, + IntegrityZomeIndex, + UsernameRegistryIntegrityLinkTypeIndex, + DeleteAgentMetadataLinkRejectionReason, +} from "@holoom/types"; +import { encode } from "@msgpack/msgpack"; +import { untilRecordKnown } from "../utils/gossip.js"; + +test("Direct user_metadata validation", async () => { + await runScenario(async (scenario) => { + const appBundleSource = await overrideHappBundle(await fakeAgentPubKey()); + const [alice, bob] = await scenario.addPlayersWithApps([ + { appBundleSource }, + { appBundleSource }, + ]); + await scenario.shareAllAgents(); + const aliceCoordinators = bindCoordinators(alice); + const bobCoordinators = bindCoordinators(bob); + + // AnyLinkableHash retypes AgentPubKey to EntryHash + const intoEntryHash = (hash: HoloHash) => + hashFrom32AndType(sliceCore32(hash), "Entry"); + + // Cannot create badly encoded metadata + try { + await aliceCoordinators.records.createLinkRaw({ + base_address: intoEntryHash(alice.agentPubKey), + target_address: intoEntryHash(alice.agentPubKey), + zome_index: IntegrityZomeIndex.UsernameRegistryIntegrity, + link_type: UsernameRegistryIntegrityLinkTypeIndex.AgentMetadata, + tag: new Uint8Array([]), + }); + expect.unreachable("Cannot create badly encoded metadata"); + } catch (err) { + expect(ValidationError.getDetail(err)).toEqual( + { + type: "CreateAgentMetadataLinkRejectionReasons", + reasons: [CreateAgentMetadataLinkRejectionReason.BadTagSerialization], + } + ); + } + + // Bob cannot create a metadata item for Alice's agent + try { + await bobCoordinators.records.createLinkRaw({ + base_address: intoEntryHash(alice.agentPubKey), + target_address: intoEntryHash(alice.agentPubKey), + zome_index: IntegrityZomeIndex.UsernameRegistryIntegrity, + link_type: UsernameRegistryIntegrityLinkTypeIndex.AgentMetadata, + tag: encode({ name: "foo", value: "bar" }), + }); + expect.unreachable("Cannot create metadata for another user"); + } catch (err) { + expect(ValidationError.getDetail(err)).toEqual( + { + type: "CreateAgentMetadataLinkRejectionReasons", + reasons: [ + CreateAgentMetadataLinkRejectionReason.BaseAddressMustBeOwner, + ], + } + ); + } + + // Alice can create a metadata item for her agent + const createLinkAh = await aliceCoordinators.records.createLinkRaw({ + base_address: intoEntryHash(alice.agentPubKey), + target_address: intoEntryHash(alice.agentPubKey), + zome_index: IntegrityZomeIndex.UsernameRegistryIntegrity, + link_type: UsernameRegistryIntegrityLinkTypeIndex.AgentMetadata, + tag: encode({ name: "foo", value: "bar" }), + }); + + await untilRecordKnown(createLinkAh, bobCoordinators); + + // Bob cannot delete Alice's metadata + try { + await bobCoordinators.records.deleteLinkRaw(createLinkAh); + expect.unreachable("Cannot delete metadata you don't own"); + } catch (err) { + expect(ValidationError.getDetail(err)).toEqual( + { + type: "DeleteAgentMetadataLinkRejectionReasons", + reasons: [DeleteAgentMetadataLinkRejectionReason.DeleterIsNotOwner], + } + ); + } + + // Alice can delete her metadata + await expect( + aliceCoordinators.records.deleteLinkRaw(createLinkAh) + ).resolves.not.toThrow(); + }); +}); diff --git a/packages/tryorama/src/user_metadata/user_only.test.ts b/packages/tryorama/src/user_metadata/user_only.test.ts new file mode 100644 index 0000000..700f2d2 --- /dev/null +++ b/packages/tryorama/src/user_metadata/user_only.test.ts @@ -0,0 +1,52 @@ +import { expect, test } from "vitest"; +import { runScenario } from "@holochain/tryorama"; + +import { overrideHappBundle } from "../utils/setup-happ.js"; +import { bindCoordinators } from "../utils/bindings.js"; +import { fakeAgentPubKey } from "@holochain/client"; +import { + ValidationRejectionDetail, + CreateAgentMetadataLinkRejectionReason, + ValidationError, +} from "@holoom/types"; + +test("Users can only update their own metadata", async () => { + await runScenario(async (scenario) => { + const appBundleSource = await overrideHappBundle(await fakeAgentPubKey()); + const [alice, bob] = await scenario.addPlayersWithApps([ + { appBundleSource }, + { appBundleSource }, + ]); + const aliceCoordinators = bindCoordinators(alice); + const bobCoordinators = bindCoordinators(bob); + await scenario.shareAllAgents(); + + // Alice starts with no metadata + await expect( + aliceCoordinators.usernameRegistry.getMetadata(alice.agentPubKey) + ).resolves.toEqual({}); + + // Alice sets an item + await expect( + aliceCoordinators.usernameRegistry.updateMetadataItem({ + name: "foo", + value: "bar2", + }) + ).resolves.not.toThrow(); + + // Bob sees new item once gossiped + while (true) { + const value = await bobCoordinators.usernameRegistry.getMetadataItemValue( + { + agent_pubkey: alice.agentPubKey, + name: "foo", + } + ); + if (value) { + expect(value).toBe("bar2"); + break; + } + await new Promise((r) => setTimeout(r, 500)); + } + }); +}); diff --git a/packages/tryorama/src/utils/bindings.ts b/packages/tryorama/src/utils/bindings.ts index c54cb03..9c62b82 100644 --- a/packages/tryorama/src/utils/bindings.ts +++ b/packages/tryorama/src/utils/bindings.ts @@ -1,11 +1,18 @@ import { AppClient } from "@holochain/client"; import { Player } from "@holochain/tryorama"; -import { UsernameRegistryCoordinator, SignerCoordinator } from "@holoom/types"; +import { + UsernameRegistryCoordinator, + SignerCoordinator, + RecordsCoordinator, +} from "@holoom/types"; export function bindCoordinators(player: Player) { const appClient = player.cells[0] as unknown as AppClient; return { + records: new RecordsCoordinator(appClient), signer: new SignerCoordinator(appClient), usernameRegistry: new UsernameRegistryCoordinator(appClient), }; } + +export type BoundCoordinators = ReturnType; diff --git a/packages/tryorama/src/utils/gossip.ts b/packages/tryorama/src/utils/gossip.ts new file mode 100644 index 0000000..e883c03 --- /dev/null +++ b/packages/tryorama/src/utils/gossip.ts @@ -0,0 +1,20 @@ +import { ActionHash, encodeHashToBase64 } from "@holochain/client"; +import { BoundCoordinators } from "./bindings"; +import { untilMsLater } from "./time"; + +export async function untilRecordKnown( + actionHash: ActionHash, + playerCoordinators: BoundCoordinators, + delay = 500, + timeout = 10_000 +) { + const deadline = Date.now() + timeout; + while (Date.now() < deadline) { + const record = await playerCoordinators.records.getRecord(actionHash); + if (record) return; + await untilMsLater(delay); + } + throw new Error( + `${encodeHashToBase64(actionHash)} not gossiped after ${timeout}ms` + ); +} diff --git a/packages/tryorama/src/utils/time.ts b/packages/tryorama/src/utils/time.ts new file mode 100644 index 0000000..90727e1 --- /dev/null +++ b/packages/tryorama/src/utils/time.ts @@ -0,0 +1,3 @@ +export function untilMsLater(ms: number) { + return new Promise((r) => setTimeout(r, ms)); +} diff --git a/packages/tryorama/vitest.config.ts b/packages/tryorama/vitest.config.ts index 2221292..7d65e76 100644 --- a/packages/tryorama/vitest.config.ts +++ b/packages/tryorama/vitest.config.ts @@ -2,6 +2,7 @@ import { defineConfig } from "vitest/config"; export default defineConfig({ test: { + poolOptions: { threads: { singleThread: true } }, testTimeout: 60 * 1000 * 3, // 3 mins }, }); diff --git a/packages/types/package.json b/packages/types/package.json index e90fcc7..213106e 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -30,7 +30,11 @@ }, "scripts": { "build": "rimraf dist && npm run build:browser && npm run build:node", - "prepare:bindings": "rimraf src/types/* && node scripts/prepare-bindings.mjs && rimraf src/zome-functions/* && node scripts/extract-fn-bindings.mjs", + "prepare:bindings": "npm run prepare:typeshare && npm run prepare:ts-rs-bindings && npm run prepare:fn-bindings && npm run prepare:integrity-enums", + "prepare:typeshare": "typeshare -l typescript -o src/typeshare-generated.ts ../../features ../../crates && tsx scripts/add-imports-to-typeshare.ts", + "prepare:ts-rs-bindings": "rimraf src/types/* && tsx scripts/prepare-bindings.ts", + "prepare:fn-bindings": "rimraf src/zome-functions/* && tsx scripts/extract-fn-bindings.ts", + "prepare:integrity-enums": "rimraf src/integrity-enums/* && tsx scripts/extract-integrity-enums.ts", "build:browser": "rollup -c rollup.browser.config.ts --configPlugin typescript", "build:node": "rollup -c rollup.node.config.ts --configPlugin typescript" }, @@ -46,7 +50,9 @@ "prettier": "^3.3.3", "rimraf": "^5.0.5", "rollup": "^4.12.0", - "rollup-plugin-cleanup": "^3.2.1" + "rollup-plugin-cleanup": "^3.2.1", + "tsx": "^4.17.0", + "yaml": "^2.5.0" }, "peerDependencies": { "@holochain/client": "^0.18.0-dev" diff --git a/packages/types/scripts/add-imports-to-typeshare.ts b/packages/types/scripts/add-imports-to-typeshare.ts new file mode 100644 index 0000000..7706ed1 --- /dev/null +++ b/packages/types/scripts/add-imports-to-typeshare.ts @@ -0,0 +1,26 @@ +import fs from "fs/promises"; +import prettier from "prettier"; +import { HOLOCHAIN_TYPES } from "./holochain-types"; + +const DEPENDENCY_TYPES_PATH = "src/dependency-types.ts"; +const TYPESHARE_GENERATED_PATH = "src/typeshare-generated.ts"; + +async function main() { + const depTypesContent = await fs.readFile(DEPENDENCY_TYPES_PATH, "utf8"); + const depTypes = Array.from( + depTypesContent.matchAll(/\nexport\s+\w+\s+(\w+)/g) + ).map((match) => match[1]); + + let content = await fs.readFile(TYPESHARE_GENERATED_PATH, "utf8"); + content = + ` + // Import prepended by scripts/add-imports-to-typeshare.ts + import {${HOLOCHAIN_TYPES.join(", ")}} from "@holochain/client"; + import {${depTypes.join(", ")}} from "./dependency-types"; + ` + content; + + content = await prettier.format(content, { parser: "typescript" }); + await fs.writeFile(TYPESHARE_GENERATED_PATH, content); +} + +main().catch(console.error); diff --git a/packages/types/scripts/extract-fn-bindings.mjs b/packages/types/scripts/extract-fn-bindings.ts similarity index 69% rename from packages/types/scripts/extract-fn-bindings.mjs rename to packages/types/scripts/extract-fn-bindings.ts index e82d6f3..fa611e5 100644 --- a/packages/types/scripts/extract-fn-bindings.mjs +++ b/packages/types/scripts/extract-fn-bindings.ts @@ -1,6 +1,7 @@ import fs from "fs/promises"; import { glob } from "glob"; import prettier from "prettier"; +import { HOLOCHAIN_TYPES } from "./holochain-types"; const CRATES_DIR = "../../crates"; const coordinator_regex = /^.+_coordinator$/; @@ -27,15 +28,35 @@ const extern_regex = const SKIPPED_METHODS = ["init", "recv_remote_signal"]; -async function extractFnBindingsForCrate(name, typesTransform) { +interface Binding { + fnName: string; + inputName: string; + inputType: string; + outputType: string; +} + +async function extractFnBindingsForCrate( + name: string, + typesTransform: TypeTransform +) { console.log("Start extracting fn bindings for:", name); const rustFiles = await glob(`${CRATES_DIR}/${name}/src/**/*.rs`); - const bindings = []; - let deps = new Set(); + const bindings: Binding[] = []; + let deps = new Set(); await Promise.all( rustFiles.map(async (filePath) => { const content = await fs.readFile(filePath, "utf-8"); - const matches = content.matchAll(extern_regex); + const matches = Array.from(content.matchAll(extern_regex)); + + const externCount = Array.from( + content.matchAll(/#\[hdk_extern\]/g) + ).length; + if (externCount !== matches.length) { + console.error( + `hdk_extern regex only captured ${matches.length} / ${externCount} occurrences in ${filePath}` + ); + } + for (const match of matches) { const [ _fullMatch, @@ -70,37 +91,42 @@ async function extractFnBindingsForCrate(name, typesTransform) { bindings.sort((x1, x2) => (x1.fnName < x2.fnName ? -1 : 1)); const methodStrs = bindings.map((binding) => { + const camelFnName = snakeToCamel(binding.fnName); if (binding.inputType === "void") { - return `async ${snakeToCamel(binding.fnName)}(): Promise<${binding.outputType}> { - return this.client.callZome({ - role_name: this.roleName, - zome_name: this.zomeName, - fn_name: "${binding.fnName}", - payload: null, - }) + return `async ${camelFnName}(): Promise<${binding.outputType}> { + return this.callFn("${binding.fnName}"); }`; } else { - return `async ${snakeToCamel(binding.fnName)}(payload: ${binding.inputType}): Promise<${binding.outputType}> { - return this.client.callZome({ - role_name: this.roleName, - zome_name: this.zomeName, - fn_name: "${binding.fnName}", - payload, - }) + const camelInputName = snakeToCamel(binding.inputName); + return `async ${camelFnName}(${camelInputName}: ${binding.inputType}): Promise<${binding.outputType}> { + return this.callFn("${binding.fnName}", ${camelInputName}); }`; } }); - const splitDeps = { holochain: ["AppClient"], holoom: [] }; + const splitDeps = { + holochain: ["AppClient"], + holoom: [], + deps: [], + typeshare: [], + }; for (const dep of deps) { const [location, name] = dep.split(":"); splitDeps[location].push(name); } let classFile = `import {${splitDeps.holochain.sort().join(", ")}} from '@holochain/client';\n`; + if (splitDeps.deps.length) { + classFile += `import {${splitDeps.deps.sort().join(", ")}} from '../dependency-types';\n`; + } + if (splitDeps.typeshare.length) { + classFile += `import {${splitDeps.typeshare.sort().join(", ")}} from '../typeshare-generated';\n`; + } if (splitDeps.holoom.length) { classFile += `import {${splitDeps.holoom.sort().join(", ")}} from '../types';\n`; } + classFile += `import { callZomeAndTransformError } from "../call-zome-helper";\n`; + const className = snakeToUpperCamel(name); classFile += ` export class ${className} { @@ -109,6 +135,16 @@ async function extractFnBindingsForCrate(name, typesTransform) { private readonly roleName = 'holoom', private readonly zomeName = '${name.replace("_coordinator", "")}', ) {} + + callFn(fn_name: string, payload?: unknown) { + return callZomeAndTransformError( + this.client, + this.roleName, + this.zomeName, + fn_name, + payload, + ); + } ${methodStrs.join("\n\n")} } @@ -130,18 +166,37 @@ const snakeToUpperCamel = (str) => { return str.charAt(0).toUpperCase() + str.slice(1); }; -const HOLOCHAIN_TYPES = ["ActionHash", "AgentPubKey", "Record", "Signature"]; - class TypeTransform { + constructor( + readonly holoomTypes: string[], + readonly depTypes: string[], + readonly typeshareTypes: string[] + ) {} + static async init() { const tsFiles = await fs.readdir(`${CRATES_DIR}/holoom_types/bindings`); const holoomTypes = tsFiles.map((name) => name.slice(0, -3)); - const transform = new TypeTransform(); - transform.holoomTypes = holoomTypes; - return transform; + + const depTypesContent = await fs.readFile( + "src/dependency-types.ts", + "utf8" + ); + const depTypes = Array.from( + depTypesContent.matchAll(/\nexport\s+\w+\s+(\w+)/g) + ).map((match) => match[1]); + + const typeshareContent = await fs.readFile( + "src/typeshare-generated.ts", + "utf8" + ); + const typeshareTypes = Array.from( + typeshareContent.matchAll(/\nexport\s+\w+\s+(\w+)/g) + ).map((match) => match[1]); + + return new TypeTransform(holoomTypes, depTypes, typeshareTypes); } - transform(rustType) { + transform(rustType): { type: string; deps: Set } { const withDelimiters = rustType.replace(/([a-z0-9]+|[^\w])/gi, "$1†"); const parts = withDelimiters .split("†") @@ -201,7 +256,7 @@ class TypeTransform { throw new Error(`Type not determined for '${rustType}'`); } - transformElemsFromParts(parts) { + transformElemsFromParts(parts): { types: string[]; deps: Set } { const rustElems = parts .join("") .split(/,\s*?/) @@ -216,9 +271,13 @@ class TypeTransform { return { types, deps }; } - transformShallow(rustType) { + transformShallow(rustType): { type: string; deps: Set } { if (HOLOCHAIN_TYPES.includes(rustType)) { return { type: rustType, deps: new Set([`holochain:${rustType}`]) }; + } else if (this.depTypes.includes(rustType)) { + return { type: rustType, deps: new Set([`dep:${rustType}`]) }; + } else if (this.typeshareTypes.includes(rustType)) { + return { type: rustType, deps: new Set([`typeshare:${rustType}`]) }; } else if (this.holoomTypes.includes(rustType)) { return { type: rustType, deps: new Set([`holoom:${rustType}`]) }; } diff --git a/packages/types/scripts/extract-integrity-enums.ts b/packages/types/scripts/extract-integrity-enums.ts new file mode 100644 index 0000000..41d65f7 --- /dev/null +++ b/packages/types/scripts/extract-integrity-enums.ts @@ -0,0 +1,176 @@ +import fs from "fs/promises"; +import { glob } from "glob"; +import yaml from "yaml"; +import prettier from "prettier"; + +const CRATES_DIR = "../../crates"; +const integrity_regex = /^.+_integrity$/; + +async function main() { + const crates = await fs.readdir(CRATES_DIR); + const classNames = await Promise.all( + crates + .filter((name) => integrity_regex.test(name)) + .map((name) => extractEntryEnumsForCrate(name)) + ); + + classNames.filter((name) => !!name).sort(); + let indexContent = classNames + .map((className) => `export * from "./${className}";\n`) + .join(""); + + const zomeIndices = await getZomeIndices(); + indexContent += ` + export enum IntegrityZomeIndex { + ${zomeIndices.map((name, idx) => `${name} = ${idx}`).join(",\n")} + } + `; + + indexContent = await prettier.format(indexContent, { parser: "typescript" }); + fs.writeFile("./src/integrity-enums/index.ts", indexContent); +} + +async function getZomeIndices() { + const yamlContent = await fs.readFile("../../workdir/dna.yaml", "utf8"); + const dnaManifest = yaml.parse(yamlContent); + return dnaManifest.integrity.zomes.map((zome) => + snakeToUpperCamel(zome.name) + ); +} + +class EntryTypesEnumParser { + state: "searching" | "enum-started" | "enum-ended" = "searching"; + defs: string[] = []; + + parseLine(line: string) { + switch (this.state) { + case "searching": { + if (line === "pub enum EntryTypes {") { + this.state = "enum-started"; + } + return; + } + case "enum-started": { + if (line === "}") { + this.state = "enum-ended"; + } else { + const match = line.match(/^\s+(\w+)\(\w+\),$/); + if (!match) { + console.error("Bad line:", line); + throw new Error("Invalid EntryType variant"); + } + this.defs.push(match[1]); + } + return; + } + case "enum-ended": { + if (line === "pub enum EntryTypes {") { + throw new Error("Repeat definition"); + } + return; + } + } + } +} + +class LinkTypesEnumParser { + state: "searching" | "enum-started" | "enum-ended" = "searching"; + defs: string[] = []; + + parseLine(line: string) { + switch (this.state) { + case "searching": { + if (line === "pub enum LinkTypes {") { + this.state = "enum-started"; + } + return; + } + case "enum-started": { + if (line === "}") { + this.state = "enum-ended"; + } else { + const match = line.match(/^\s+(\w+),$/); + if (!match) { + console.error("Bad line:", line); + throw new Error("Invalid LinkType variant"); + } + this.defs.push(match[1]); + } + return; + } + case "enum-ended": { + if (line === "pub enum LinkTypes {") { + throw new Error("Repeat definition"); + } + return; + } + } + } +} + +async function extractEntryEnumsForCrate(name: string) { + console.log("Start extracting entry enums for:", name); + const rustFiles = await glob(`${CRATES_DIR}/${name}/src/**/*.rs`); + let entryDefs: string[] | undefined; + let linkDefs: string[] | undefined; + await Promise.all( + rustFiles.map(async (filePath) => { + const lines = (await fs.readFile(filePath, "utf-8")).split("\n"); + const entryTypesParser = new EntryTypesEnumParser(); + const linkTypesParser = new LinkTypesEnumParser(); + for (const line of lines) { + entryTypesParser.parseLine(line); + linkTypesParser.parseLine(line); + } + if (entryTypesParser.state === "enum-ended") { + if (entryDefs) { + throw new Error("entry defs already parsed"); + } + entryDefs = entryTypesParser.defs; + } + if (linkTypesParser.state === "enum-ended") { + if (linkDefs) { + throw new Error("link defs already parsed"); + } + linkDefs = linkTypesParser.defs; + } + }) + ); + + if (!entryDefs && !linkDefs) return; + + const zomeNameUpper = snakeToUpperCamel(name); + let content = ""; + if (entryDefs?.length) { + content += ` + export enum ${zomeNameUpper}EntryTypeIndex { + ${entryDefs.map((def, idx) => `${def} = ${idx},`).join("\n")} + } + `; + } + if (linkDefs?.length) { + content += ` + export enum ${zomeNameUpper}LinkTypeIndex { + ${linkDefs.map((def, idx) => `${def} = ${idx},`).join("\n")} + } + `; + } + + content = await prettier.format(content, { parser: "typescript" }); + await fs.writeFile(`./src/integrity-enums/${zomeNameUpper}.ts`, content); + return zomeNameUpper; +} + +const snakeToCamel = (str) => + str + .toLowerCase() + .replace(/([-_][a-z])/g, (group) => + group.toUpperCase().replace("-", "").replace("_", "") + ); + +const snakeToUpperCamel = (str) => { + str = snakeToCamel(str); + return str.charAt(0).toUpperCase() + str.slice(1); +}; + +main().catch(console.error); diff --git a/packages/types/scripts/holochain-types.ts b/packages/types/scripts/holochain-types.ts new file mode 100644 index 0000000..491d921 --- /dev/null +++ b/packages/types/scripts/holochain-types.ts @@ -0,0 +1,10 @@ +export const HOLOCHAIN_TYPES = [ + "ActionHash", + "AgentPubKey", + "Record", + "Signature", + "ZomeIndex", + "LinkTag", + "LinkType", + "AnyLinkableHash", +]; diff --git a/packages/types/scripts/prepare-bindings.mjs b/packages/types/scripts/prepare-bindings.ts similarity index 85% rename from packages/types/scripts/prepare-bindings.mjs rename to packages/types/scripts/prepare-bindings.ts index eb4047e..4a41ac7 100644 --- a/packages/types/scripts/prepare-bindings.mjs +++ b/packages/types/scripts/prepare-bindings.ts @@ -1,15 +1,17 @@ import fs from "fs/promises"; import prettier from "prettier"; +import { HOLOCHAIN_TYPES } from "./holochain-types"; const BINDINGS_PATH = "../../crates/holoom_types/bindings/"; -const HC_TYPES = ["AgentPubKey", "ActionHash", "Record", "Signature"]; async function main() { const files = await fs.readdir(BINDINGS_PATH); for (const file of files) { // Insert missing imports let content = await fs.readFile(BINDINGS_PATH + file, "utf8"); - const imports = HC_TYPES.filter((typeName) => content.includes(typeName)); + const imports = HOLOCHAIN_TYPES.filter((typeName) => + content.includes(typeName) + ); if (imports.length) { // Prepend imports const importLine = `import { ${imports.join( diff --git a/packages/types/src/call-zome-helper.ts b/packages/types/src/call-zome-helper.ts new file mode 100644 index 0000000..62906ef --- /dev/null +++ b/packages/types/src/call-zome-helper.ts @@ -0,0 +1,37 @@ +import { ValidationError } from "./errors"; +import { AppClient } from "@holochain/client"; + +const MISSING_ACTION_REGEX = + /Source chain error: InvalidCommit error: The dependency AnyDhtHash\(uhCkk[^\)]{48}\) was not found on the DHT/; + +export async function callZomeAndTransformError( + client: AppClient, + role_name: string, + zome_name: string, + fn_name: string, + payload: unknown +) { + const invoke = () => + client + .callZome({ + role_name, + zome_name, + fn_name, + payload, + }) + .catch(ValidationError.tryCastThrow); + try { + const result = await invoke(); + return result; + } catch (err) { + if (err instanceof Error && MISSING_ACTION_REGEX.test(err.message)) { + // This appears to be some timing related error. So far I've only seen + // it occur in CI. For now we'll allow one retry after a short wait as a + // work around. + await new Promise((r) => setTimeout(r, 500)); + const result = await invoke(); + return result; + } + throw err; + } +} diff --git a/packages/types/src/dependency-types.ts b/packages/types/src/dependency-types.ts new file mode 100644 index 0000000..68e5f78 --- /dev/null +++ b/packages/types/src/dependency-types.ts @@ -0,0 +1,11 @@ +// typeshare isn't able to generate types outside scope of the source code +// being parsed. Instead, we hand-author them here. We don't currently have a +// strategy for ensuring that these types stay in sync with the external rust +// crates that they represent types from. + +// Type alias for `alloy_primitives::Address` +export type EthAddress = Uint8Array; + +// (I don't know why these aren't exported from @holochain/client) +export type EntryDefIndex = number; +export type SerializedBytes = Uint8Array; diff --git a/packages/types/src/errors.ts b/packages/types/src/errors.ts new file mode 100644 index 0000000..1c14878 --- /dev/null +++ b/packages/types/src/errors.ts @@ -0,0 +1,42 @@ +import { ValidationRejectionDetail } from "./typeshare-generated"; + +export class ValidationError extends Error { + constructor( + message: string, + readonly detail: ValidationRejectionDetail + ) { + super(message); + } + + static tryCast(error: Error): Error { + if (error.message.includes("InvalidCommit")) { + const fail = () => { + console.error( + `Badly formed invalid commit reasons message: ${error.message}` + ); + return error; + }; + const parts1 = error.message.split("__REASONS_START__"); + if (parts1.length !== 2) return fail(); + const parts2 = parts1[1].split("__REASONS_END__"); + if (parts2.length !== 2) return fail(); + try { + const detail = JSON.parse(parts2[0]); + return new ValidationError(error.message, detail); + } catch { + return fail(); + } + } + return error; + } + + static tryCastThrow(error: Error): never { + throw ValidationError.tryCast(error); + } + + static getDetail(error: unknown) { + if (error instanceof ValidationError) { + return error.detail; + } + } +} diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 94c610c..97ec2d5 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -1,2 +1,6 @@ export * from "./types"; export * from "./zome-functions"; +export * from "./typeshare-generated"; +export * from "./dependency-types"; +export * from "./integrity-enums"; +export * from "./errors"; diff --git a/packages/types/src/integrity-enums/UsernameRegistryIntegrity.ts b/packages/types/src/integrity-enums/UsernameRegistryIntegrity.ts new file mode 100644 index 0000000..934d36f --- /dev/null +++ b/packages/types/src/integrity-enums/UsernameRegistryIntegrity.ts @@ -0,0 +1,22 @@ +export enum UsernameRegistryIntegrityEntryTypeIndex { + UsernameAttestation = 0, + WalletAttestation = 1, + ExternalIdAttestation = 2, + OracleDocument = 3, + Recipe = 4, + RecipeExecution = 5, + SignedEvmSigningOffer = 6, +} + +export enum UsernameRegistryIntegrityLinkTypeIndex { + AgentToUsernameAttestations = 0, + AgentMetadata = 1, + AgentToWalletAttestations = 2, + AgentToExternalIdAttestation = 3, + ExternalIdToAttestation = 4, + NameToOracleDocument = 5, + RelateOracleDocumentName = 6, + NameToRecipe = 7, + NameToSigningOffer = 8, + EvmAddressToSigningOffer = 9, +} diff --git a/packages/types/src/integrity-enums/index.ts b/packages/types/src/integrity-enums/index.ts new file mode 100644 index 0000000..c9f8732 --- /dev/null +++ b/packages/types/src/integrity-enums/index.ts @@ -0,0 +1,5 @@ +export * from "./UsernameRegistryIntegrity"; + +export enum IntegrityZomeIndex { + UsernameRegistryIntegrity = 0, +} diff --git a/packages/types/src/types/GetMetadataItemValuePayload.ts b/packages/types/src/types/GetMetadataItemValuePayload.ts deleted file mode 100644 index 12ba4eb..0000000 --- a/packages/types/src/types/GetMetadataItemValuePayload.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { AgentPubKey } from "@holochain/client"; -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type GetMetadataItemValuePayload = { - agent_pubkey: AgentPubKey; - name: string; -}; diff --git a/packages/types/src/types/MetadataItem.ts b/packages/types/src/types/MetadataItem.ts deleted file mode 100644 index 3aa80a0..0000000 --- a/packages/types/src/types/MetadataItem.ts +++ /dev/null @@ -1,3 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type MetadataItem = { name: string; value: string }; diff --git a/packages/types/src/types/UpdateMetadataItemPayload.ts b/packages/types/src/types/UpdateMetadataItemPayload.ts deleted file mode 100644 index 2fe23a7..0000000 --- a/packages/types/src/types/UpdateMetadataItemPayload.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { AgentPubKey } from "@holochain/client"; -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type UpdateMetadataItemPayload = { - agent_pubkey: AgentPubKey; - name: string; - value: string; -}; diff --git a/packages/types/src/types/WalletAttestation.ts b/packages/types/src/types/WalletAttestation.ts index 019d207..d860d2f 100644 --- a/packages/types/src/types/WalletAttestation.ts +++ b/packages/types/src/types/WalletAttestation.ts @@ -1,4 +1,4 @@ -import { AgentPubKey, ActionHash, Signature } from "@holochain/client"; +import { ActionHash, AgentPubKey, Signature } from "@holochain/client"; // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { ChainWalletSignature } from "./ChainWalletSignature"; diff --git a/packages/types/src/types/index.ts b/packages/types/src/types/index.ts index 9ad428a..9f2144b 100644 --- a/packages/types/src/types/index.ts +++ b/packages/types/src/types/index.ts @@ -7,11 +7,9 @@ export * from "./EvmSigningOffer"; export * from "./EvmU256Item"; export * from "./ExecuteRecipePayload"; export * from "./ExternalIdAttestation"; -export * from "./GetMetadataItemValuePayload"; export * from "./IngestExternalIdAttestationRequestPayload"; export * from "./JqInstructionArgumentNames"; export * from "./LocalHoloomSignal"; -export * from "./MetadataItem"; export * from "./OracleDocument"; export * from "./Recipe"; export * from "./RecipeArgument"; @@ -28,6 +26,5 @@ export * from "./SignableBytes"; export * from "./SignedEvmSigningOffer"; export * from "./SignedEvmU256Array"; export * from "./SignedUsername"; -export * from "./UpdateMetadataItemPayload"; export * from "./UsernameAttestation"; export * from "./WalletAttestation"; diff --git a/packages/types/src/typeshare-generated.ts b/packages/types/src/typeshare-generated.ts new file mode 100644 index 0000000..ed3d57a --- /dev/null +++ b/packages/types/src/typeshare-generated.ts @@ -0,0 +1,91 @@ +// Import prepended by scripts/add-imports-to-typeshare.ts +import { + ActionHash, + AgentPubKey, + Record, + Signature, + ZomeIndex, + LinkTag, + LinkType, + AnyLinkableHash, +} from "@holochain/client"; +import { EthAddress, EntryDefIndex, SerializedBytes } from "./dependency-types"; +/* + Generated by typeshare 1.9.2 +*/ + +/** A key-value pair, representing an item of a user's metadata */ +export interface MetadataItem { + name: string; + value: string; +} + +/** Input to the `create_app_entry_raw` function */ +export interface CreateAppEntryRawInput { + /** The index of the zome that defines the entry type */ + zome_index: ZomeIndex; + /** The index of the entry definition within the zome */ + entry_def_index: EntryDefIndex; + /** The msgpack serialised app entry content */ + entry_bytes: SerializedBytes; +} + +/** Input to the `create_link_raw` function */ +export interface CreateLinkRawInput { + /** The 'form' address of the link */ + base_address: AnyLinkableHash; + /** The 'to' address of the link */ + target_address: AnyLinkableHash; + /** The index of the zome in which the link was defined */ + zome_index: ZomeIndex; + /** The index of the link definition within the zome */ + link_type: LinkType; + /** Freeform data attached to the link */ + tag: LinkTag; +} + +/** The input argument to `update_metadata_item`` */ +export interface UpdateMetadataItemInput { + /** The key for the particular metadata item */ + name: string; + /** The value to assign to the key */ + value: string; +} + +/** The input argument to `get_metadata_item_value`` */ +export interface GetMetadataItemValueInput { + /** The agent whose metadata you wish to query */ + agent_pubkey: AgentPubKey; + /** The key for the particular metadata item */ + name: string; +} + +/** Reasons for which a create `AgentMetadata` link action can fail validation. */ +export enum CreateAgentMetadataLinkRejectionReason { + /** + * The base address is the agent pubkey of the user who is being annotated with metadata. + * As a user can only author their own metadata, the base address has match their own pubkey. + */ + BaseAddressMustBeOwner = "BaseAddressMustBeOwner", + /** The link tag content doesn't match the expected key-value schema struct `MetadataItem`. */ + BadTagSerialization = "BadTagSerialization", +} + +/** Reasons for which a delete `AgentMetadata` link action can fail validation. */ +export enum DeleteAgentMetadataLinkRejectionReason { + /** + * The user attempting to delete the metadata item is not the owner and therefore doesn't + * have permission. + */ + DeleterIsNotOwner = "DeleterIsNotOwner", +} + +export type ValidationRejectionDetail = + | { + type: "CreateAgentMetadataLinkRejectionReasons"; + reasons: CreateAgentMetadataLinkRejectionReason[]; + } + | { + type: "DeleteAgentMetadataLinkRejectionReasons"; + reasons: DeleteAgentMetadataLinkRejectionReason[]; + }; diff --git a/packages/types/src/zome-functions/PingCoordinator.ts b/packages/types/src/zome-functions/PingCoordinator.ts index af9c334..5a9130a 100644 --- a/packages/types/src/zome-functions/PingCoordinator.ts +++ b/packages/types/src/zome-functions/PingCoordinator.ts @@ -1,4 +1,5 @@ import { AppClient } from "@holochain/client"; +import { callZomeAndTransformError } from "../call-zome-helper"; export class PingCoordinator { constructor( @@ -7,12 +8,17 @@ export class PingCoordinator { private readonly zomeName = "ping", ) {} + callFn(fn_name: string, payload?: unknown) { + return callZomeAndTransformError( + this.client, + this.roleName, + this.zomeName, + fn_name, + payload, + ); + } + async ping(): Promise { - return this.client.callZome({ - role_name: this.roleName, - zome_name: this.zomeName, - fn_name: "ping", - payload: null, - }); + return this.callFn("ping"); } } diff --git a/packages/types/src/zome-functions/RecordsCoordinator.ts b/packages/types/src/zome-functions/RecordsCoordinator.ts index e8f1f02..a5fe4cb 100644 --- a/packages/types/src/zome-functions/RecordsCoordinator.ts +++ b/packages/types/src/zome-functions/RecordsCoordinator.ts @@ -1,4 +1,9 @@ import { ActionHash, AppClient, Record } from "@holochain/client"; +import { + CreateAppEntryRawInput, + CreateLinkRawInput, +} from "../typeshare-generated"; +import { callZomeAndTransformError } from "../call-zome-helper"; export class RecordsCoordinator { constructor( @@ -7,12 +12,33 @@ export class RecordsCoordinator { private readonly zomeName = "records", ) {} - async getRecord(payload: ActionHash): Promise { - return this.client.callZome({ - role_name: this.roleName, - zome_name: this.zomeName, - fn_name: "get_record", + callFn(fn_name: string, payload?: unknown) { + return callZomeAndTransformError( + this.client, + this.roleName, + this.zomeName, + fn_name, payload, - }); + ); + } + + async createAppEntryRaw(input: CreateAppEntryRawInput): Promise { + return this.callFn("create_app_entry_raw", input); + } + + async createLinkRaw(input: CreateLinkRawInput): Promise { + return this.callFn("create_link_raw", input); + } + + async deleteEntryRaw(actionHash: ActionHash): Promise { + return this.callFn("delete_entry_raw", actionHash); + } + + async deleteLinkRaw(createLinkActionHash: ActionHash): Promise { + return this.callFn("delete_link_raw", createLinkActionHash); + } + + async getRecord(actionHash: ActionHash): Promise { + return this.callFn("get_record", actionHash); } } diff --git a/packages/types/src/zome-functions/SignerCoordinator.ts b/packages/types/src/zome-functions/SignerCoordinator.ts index 8427917..bc3115e 100644 --- a/packages/types/src/zome-functions/SignerCoordinator.ts +++ b/packages/types/src/zome-functions/SignerCoordinator.ts @@ -1,5 +1,6 @@ import { AppClient, Signature } from "@holochain/client"; import { SignableBytes } from "../types"; +import { callZomeAndTransformError } from "../call-zome-helper"; export class SignerCoordinator { constructor( @@ -8,12 +9,17 @@ export class SignerCoordinator { private readonly zomeName = "signer", ) {} - async signMessage(payload: SignableBytes): Promise { - return this.client.callZome({ - role_name: this.roleName, - zome_name: this.zomeName, - fn_name: "sign_message", + callFn(fn_name: string, payload?: unknown) { + return callZomeAndTransformError( + this.client, + this.roleName, + this.zomeName, + fn_name, payload, - }); + ); + } + + async signMessage(message: SignableBytes): Promise { + return this.callFn("sign_message", message); } } diff --git a/packages/types/src/zome-functions/UsernameRegistryCoordinator.ts b/packages/types/src/zome-functions/UsernameRegistryCoordinator.ts index 63d06c1..7140238 100644 --- a/packages/types/src/zome-functions/UsernameRegistryCoordinator.ts +++ b/packages/types/src/zome-functions/UsernameRegistryCoordinator.ts @@ -1,4 +1,8 @@ import { ActionHash, AgentPubKey, AppClient, Record } from "@holochain/client"; +import { + GetMetadataItemValueInput, + UpdateMetadataItemInput, +} from "../typeshare-generated"; import { ChainWalletSignature, ConfirmExternalIdRequestPayload, @@ -7,7 +11,6 @@ import { EvmSignatureOverRecipeExecutionRequest, ExecuteRecipePayload, ExternalIdAttestation, - GetMetadataItemValuePayload, IngestExternalIdAttestationRequestPayload, OracleDocument, Recipe, @@ -17,10 +20,10 @@ import { ResolveEvmSignatureOverRecipeExecutionRequestPayload, SendExternalIdAttestationRequestPayload, SignedUsername, - UpdateMetadataItemPayload, UsernameAttestation, WalletAttestation, } from "../types"; +import { callZomeAndTransformError } from "../call-zome-helper"; export class UsernameRegistryCoordinator { constructor( @@ -29,424 +32,257 @@ export class UsernameRegistryCoordinator { private readonly zomeName = "username_registry", ) {} - async attestWalletSignature(payload: ChainWalletSignature): Promise { - return this.client.callZome({ - role_name: this.roleName, - zome_name: this.zomeName, - fn_name: "attest_wallet_signature", + callFn(fn_name: string, payload?: unknown) { + return callZomeAndTransformError( + this.client, + this.roleName, + this.zomeName, + fn_name, payload, - }); + ); + } + + async attestWalletSignature( + chainWalletSignature: ChainWalletSignature, + ): Promise { + return this.callFn("attest_wallet_signature", chainWalletSignature); } async confirmExternalIdRequest( payload: ConfirmExternalIdRequestPayload, ): Promise { - return this.client.callZome({ - role_name: this.roleName, - zome_name: this.zomeName, - fn_name: "confirm_external_id_request", - payload, - }); + return this.callFn("confirm_external_id_request", payload); } async createExternalIdAttestation( - payload: ExternalIdAttestation, + attestation: ExternalIdAttestation, ): Promise { - return this.client.callZome({ - role_name: this.roleName, - zome_name: this.zomeName, - fn_name: "create_external_id_attestation", - payload, - }); + return this.callFn("create_external_id_attestation", attestation); } - async createOracleDocument(payload: OracleDocument): Promise { - return this.client.callZome({ - role_name: this.roleName, - zome_name: this.zomeName, - fn_name: "create_oracle_document", - payload, - }); + async createOracleDocument(oracleDocument: OracleDocument): Promise { + return this.callFn("create_oracle_document", oracleDocument); } - async createRecipe(payload: Recipe): Promise { - return this.client.callZome({ - role_name: this.roleName, - zome_name: this.zomeName, - fn_name: "create_recipe", - payload, - }); + async createRecipe(recipe: Recipe): Promise { + return this.callFn("create_recipe", recipe); } - async createRecipeExecution(payload: RecipeExecution): Promise { - return this.client.callZome({ - role_name: this.roleName, - zome_name: this.zomeName, - fn_name: "create_recipe_execution", - payload, - }); + async createRecipeExecution( + recipeExecution: RecipeExecution, + ): Promise { + return this.callFn("create_recipe_execution", recipeExecution); } async createSignedEvmSigningOffer( payload: CreateEvmSigningOfferPayload, ): Promise { - return this.client.callZome({ - role_name: this.roleName, - zome_name: this.zomeName, - fn_name: "create_signed_evm_signing_offer", - payload, - }); + return this.callFn("create_signed_evm_signing_offer", payload); } async createUsernameAttestation( - payload: UsernameAttestation, + usernameAttestation: UsernameAttestation, ): Promise { - return this.client.callZome({ - role_name: this.roleName, - zome_name: this.zomeName, - fn_name: "create_username_attestation", - payload, - }); + return this.callFn("create_username_attestation", usernameAttestation); } - async createWalletAttestation(payload: WalletAttestation): Promise { - return this.client.callZome({ - role_name: this.roleName, - zome_name: this.zomeName, - fn_name: "create_wallet_attestation", - payload, - }); + async createWalletAttestation( + walletAttestation: WalletAttestation, + ): Promise { + return this.callFn("create_wallet_attestation", walletAttestation); } - async deleteExternalIdAttestation(payload: ActionHash): Promise { - return this.client.callZome({ - role_name: this.roleName, - zome_name: this.zomeName, - fn_name: "delete_external_id_attestation", - payload, - }); + async deleteExternalIdAttestation( + originalAttestationHash: ActionHash, + ): Promise { + return this.callFn( + "delete_external_id_attestation", + originalAttestationHash, + ); } - async deleteUsernameAttestation(payload: ActionHash): Promise { - return this.client.callZome({ - role_name: this.roleName, - zome_name: this.zomeName, - fn_name: "delete_username_attestation", - payload, - }); + async deleteUsernameAttestation( + originalUsernameAttestationHash: ActionHash, + ): Promise { + return this.callFn( + "delete_username_attestation", + originalUsernameAttestationHash, + ); } - async doesAgentHaveUsername(payload: AgentPubKey): Promise { - return this.client.callZome({ - role_name: this.roleName, - zome_name: this.zomeName, - fn_name: "does_agent_have_username", - payload, - }); + async doesAgentHaveUsername(agent: AgentPubKey): Promise { + return this.callFn("does_agent_have_username", agent); } async executeRecipe(payload: ExecuteRecipePayload): Promise { - return this.client.callZome({ - role_name: this.roleName, - zome_name: this.zomeName, - fn_name: "execute_recipe", - payload, - }); + return this.callFn("execute_recipe", payload); } async getAllExternalIdAhs(): Promise { - return this.client.callZome({ - role_name: this.roleName, - zome_name: this.zomeName, - fn_name: "get_all_external_id_ahs", - payload: null, - }); + return this.callFn("get_all_external_id_ahs"); } async getAllUsernameAttestations(): Promise { - return this.client.callZome({ - role_name: this.roleName, - zome_name: this.zomeName, - fn_name: "get_all_username_attestations", - payload: null, - }); - } - - async getAttestationForExternalId(payload: string): Promise { - return this.client.callZome({ - role_name: this.roleName, - zome_name: this.zomeName, - fn_name: "get_attestation_for_external_id", - payload, - }); + return this.callFn("get_all_username_attestations"); + } + + async getAttestationForExternalId( + externalId: string, + ): Promise { + return this.callFn("get_attestation_for_external_id", externalId); } async getAuthority(): Promise { - return this.client.callZome({ - role_name: this.roleName, - zome_name: this.zomeName, - fn_name: "get_authority", - payload: null, - }); - } - - async getEvmWalletBindingMessage(payload: Uint8Array): Promise { - return this.client.callZome({ - role_name: this.roleName, - zome_name: this.zomeName, - fn_name: "get_evm_wallet_binding_message", - payload, - }); + return this.callFn("get_authority"); } - async getExternalIdAttestation(payload: ActionHash): Promise { - return this.client.callZome({ - role_name: this.roleName, - zome_name: this.zomeName, - fn_name: "get_external_id_attestation", - payload, - }); + async getEvmWalletBindingMessage(evmAddress: Uint8Array): Promise { + return this.callFn("get_evm_wallet_binding_message", evmAddress); + } + + async getExternalIdAttestation( + externalIdAh: ActionHash, + ): Promise { + return this.callFn("get_external_id_attestation", externalIdAh); } async getExternalIdAttestationsForAgent( - payload: AgentPubKey, + agentPubkey: AgentPubKey, ): Promise { - return this.client.callZome({ - role_name: this.roleName, - zome_name: this.zomeName, - fn_name: "get_external_id_attestations_for_agent", - payload, - }); + return this.callFn("get_external_id_attestations_for_agent", agentPubkey); } async getLatestEvmSigningOfferAhForName( - payload: string, + name: string, ): Promise { - return this.client.callZome({ - role_name: this.roleName, - zome_name: this.zomeName, - fn_name: "get_latest_evm_signing_offer_ah_for_name", - payload, - }); + return this.callFn("get_latest_evm_signing_offer_ah_for_name", name); } - async getMetadata(payload: AgentPubKey): Promise<{ [key: string]: string }> { - return this.client.callZome({ - role_name: this.roleName, - zome_name: this.zomeName, - fn_name: "get_metadata", - payload, - }); + async getMetadata( + agentPubkey: AgentPubKey, + ): Promise<{ [key: string]: string }> { + return this.callFn("get_metadata", agentPubkey); } async getMetadataItemValue( - payload: GetMetadataItemValuePayload, + input: GetMetadataItemValueInput, ): Promise { - return this.client.callZome({ - role_name: this.roleName, - zome_name: this.zomeName, - fn_name: "get_metadata_item_value", - payload, - }); + return this.callFn("get_metadata_item_value", input); } - async getOracleDocumentLinkAhsForName( - payload: string, - ): Promise { - return this.client.callZome({ - role_name: this.roleName, - zome_name: this.zomeName, - fn_name: "get_oracle_document_link_ahs_for_name", - payload, - }); + async getOracleDocumentLinkAhsForName(name: string): Promise { + return this.callFn("get_oracle_document_link_ahs_for_name", name); } - async getRelatedOracleDocumentNames(payload: string): Promise { - return this.client.callZome({ - role_name: this.roleName, - zome_name: this.zomeName, - fn_name: "get_related_oracle_document_names", - payload, - }); + async getRelatedOracleDocumentNames(relationName: string): Promise { + return this.callFn("get_related_oracle_document_names", relationName); } - async getRelationLinkAhs(payload: string): Promise { - return this.client.callZome({ - role_name: this.roleName, - zome_name: this.zomeName, - fn_name: "get_relation_link_ahs", - payload, - }); + async getRelationLinkAhs(relationName: string): Promise { + return this.callFn("get_relation_link_ahs", relationName); } async getSigningOfferAhsForEvmAddress( - payload: Uint8Array, + evmAddress: Uint8Array, ): Promise { - return this.client.callZome({ - role_name: this.roleName, - zome_name: this.zomeName, - fn_name: "get_signing_offer_ahs_for_evm_address", - payload, - }); + return this.callFn("get_signing_offer_ahs_for_evm_address", evmAddress); } - async getSolanaWalletBindingMessage(payload: Uint8Array): Promise { - return this.client.callZome({ - role_name: this.roleName, - zome_name: this.zomeName, - fn_name: "get_solana_wallet_binding_message", - payload, - }); + async getSolanaWalletBindingMessage( + solanaAddress: Uint8Array, + ): Promise { + return this.callFn("get_solana_wallet_binding_message", solanaAddress); } - async getUsernameAttestation(payload: ActionHash): Promise { - return this.client.callZome({ - role_name: this.roleName, - zome_name: this.zomeName, - fn_name: "get_username_attestation", - payload, - }); + async getUsernameAttestation( + usernameAttestationHash: ActionHash, + ): Promise { + return this.callFn("get_username_attestation", usernameAttestationHash); } async getUsernameAttestationForAgent( - payload: AgentPubKey, + agent: AgentPubKey, ): Promise { - return this.client.callZome({ - role_name: this.roleName, - zome_name: this.zomeName, - fn_name: "get_username_attestation_for_agent", - payload, - }); + return this.callFn("get_username_attestation_for_agent", agent); } - async getWalletAttestation(payload: ActionHash): Promise { - return this.client.callZome({ - role_name: this.roleName, - zome_name: this.zomeName, - fn_name: "get_wallet_attestation", - payload, - }); + async getWalletAttestation( + walletAttestationHash: ActionHash, + ): Promise { + return this.callFn("get_wallet_attestation", walletAttestationHash); } - async getWalletAttestationsForAgent(payload: AgentPubKey): Promise { - return this.client.callZome({ - role_name: this.roleName, - zome_name: this.zomeName, - fn_name: "get_wallet_attestations_for_agent", - payload, - }); + async getWalletAttestationsForAgent(agent: AgentPubKey): Promise { + return this.callFn("get_wallet_attestations_for_agent", agent); } async ingestEvmSignatureOverRecipeExecutionRequest( payload: EvmSignatureOverRecipeExecutionRequest, ): Promise { - return this.client.callZome({ - role_name: this.roleName, - zome_name: this.zomeName, - fn_name: "ingest_evm_signature_over_recipe_execution_request", + return this.callFn( + "ingest_evm_signature_over_recipe_execution_request", payload, - }); + ); } async ingestExternalIdAttestationRequest( payload: IngestExternalIdAttestationRequestPayload, ): Promise { - return this.client.callZome({ - role_name: this.roleName, - zome_name: this.zomeName, - fn_name: "ingest_external_id_attestation_request", - payload, - }); + return this.callFn("ingest_external_id_attestation_request", payload); } - async ingestSignedUsername(payload: SignedUsername): Promise { - return this.client.callZome({ - role_name: this.roleName, - zome_name: this.zomeName, - fn_name: "ingest_signed_username", - payload, - }); + async ingestSignedUsername(signedUsername: SignedUsername): Promise { + return this.callFn("ingest_signed_username", signedUsername); } async rejectEvmSignatureOverRecipeExecutionRequest( payload: RejectEvmSignatureOverRecipeExecutionRequestPayload, ): Promise { - return this.client.callZome({ - role_name: this.roleName, - zome_name: this.zomeName, - fn_name: "reject_evm_signature_over_recipe_execution_request", + return this.callFn( + "reject_evm_signature_over_recipe_execution_request", payload, - }); + ); } async rejectExternalIdRequest( payload: RejectExternalIdRequestPayload, ): Promise { - return this.client.callZome({ - role_name: this.roleName, - zome_name: this.zomeName, - fn_name: "reject_external_id_request", - payload, - }); + return this.callFn("reject_external_id_request", payload); } - async relateOracleDocument(payload: DocumentRelationTag): Promise { - return this.client.callZome({ - role_name: this.roleName, - zome_name: this.zomeName, - fn_name: "relate_oracle_document", - payload, - }); + async relateOracleDocument(relationTag: DocumentRelationTag): Promise { + return this.callFn("relate_oracle_document", relationTag); } async resolveEvmSignatureOverRecipeExecutionRequest( payload: ResolveEvmSignatureOverRecipeExecutionRequestPayload, ): Promise { - return this.client.callZome({ - role_name: this.roleName, - zome_name: this.zomeName, - fn_name: "resolve_evm_signature_over_recipe_execution_request", + return this.callFn( + "resolve_evm_signature_over_recipe_execution_request", payload, - }); + ); } async sendExternalIdAttestationRequest( payload: SendExternalIdAttestationRequestPayload, ): Promise { - return this.client.callZome({ - role_name: this.roleName, - zome_name: this.zomeName, - fn_name: "send_external_id_attestation_request", - payload, - }); + return this.callFn("send_external_id_attestation_request", payload); } async sendRequestForEvmSignatureOverRecipeExecution( - payload: EvmSignatureOverRecipeExecutionRequest, + request: EvmSignatureOverRecipeExecutionRequest, ): Promise { - return this.client.callZome({ - role_name: this.roleName, - zome_name: this.zomeName, - fn_name: "send_request_for_evm_signature_over_recipe_execution", - payload, - }); + return this.callFn( + "send_request_for_evm_signature_over_recipe_execution", + request, + ); } - async signUsernameToAttest(payload: string): Promise { - return this.client.callZome({ - role_name: this.roleName, - zome_name: this.zomeName, - fn_name: "sign_username_to_attest", - payload, - }); + async signUsernameToAttest(username: string): Promise { + return this.callFn("sign_username_to_attest", username); } - async updateMetadataItem(payload: UpdateMetadataItemPayload): Promise { - return this.client.callZome({ - role_name: this.roleName, - zome_name: this.zomeName, - fn_name: "update_metadata_item", - payload, - }); + async updateMetadataItem(input: UpdateMetadataItemInput): Promise { + return this.callFn("update_metadata_item", input); } }