diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 405db15..16f3cce 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -26,7 +26,7 @@ If applicable, add screenshots to help explain your problem. **Desktop (please complete the following information):** - OS: [e.g. iOS] - Browser [e.g. chrome, safari] - - Kinode version [e.g. v1.0.0] + - Hyperware version [e.g. v1.0.0] - process_lib version [e.g. v1.0.0] **Additional context** diff --git a/Cargo.lock b/Cargo.lock index a233a1f..54ac3bd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -60,6 +60,7 @@ dependencies = [ "alloy-rpc-client", "alloy-rpc-types", "alloy-serde", + "alloy-transport", "alloy-transport-http", ] @@ -1546,6 +1547,33 @@ dependencies = [ "tracing", ] +[[package]] +name = "hyperware_process_lib" +version = "1.2.0" +dependencies = [ + "alloy", + "alloy-primitives", + "alloy-sol-macro", + "alloy-sol-types", + "anyhow", + "base64", + "bincode", + "color-eyre", + "http", + "mime_guess", + "rand", + "regex", + "rmp-serde", + "serde", + "serde_json", + "thiserror 1.0.69", + "tracing", + "tracing-error", + "tracing-subscriber", + "url", + "wit-bindgen", +] + [[package]] name = "icu_collections" version = "1.5.0" @@ -1800,32 +1828,6 @@ dependencies = [ "sha3-asm", ] -[[package]] -name = "kinode_process_lib" -version = "1.0.0" -dependencies = [ - "alloy", - "alloy-primitives", - "alloy-sol-macro", - "alloy-sol-types", - "anyhow", - "bincode", - "color-eyre", - "http", - "mime_guess", - "rand", - "regex", - "rmp-serde", - "serde", - "serde_json", - "thiserror 1.0.69", - "tracing", - "tracing-error", - "tracing-subscriber", - "url", - "wit-bindgen", -] - [[package]] name = "lazy_static" version = "1.5.0" diff --git a/Cargo.toml b/Cargo.toml index 797fabf..086abe9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,15 +1,16 @@ [package] -name = "kinode_process_lib" +name = "hyperware_process_lib" authors = ["Sybil Technologies AG"] -version = "1.0.0" +version = "1.2.0" edition = "2021" -description = "A library for writing Kinode processes in Rust." -homepage = "https://kinode.org" -repository = "https://github.com/kinode-dao/process_lib" +description = "A library for writing Hyperware processes in Rust." +homepage = "https://hyperware.ai" +repository = "https://github.com/hyperware-ai/process_lib" license = "Apache-2.0" [features] logging = ["dep:color-eyre", "dep:tracing", "dep:tracing-error", "dep:tracing-subscriber"] +simulation-mode = [] [dependencies] alloy-primitives = "0.8.15" @@ -17,9 +18,11 @@ alloy-sol-macro = "0.8.15" alloy-sol-types = "0.8.15" alloy = { version = "0.8.1", features = [ "json-rpc", + "rpc-client", "rpc-types", ] } anyhow = "1.0" +base64 = "0.22.1" bincode = "1.3.3" color-eyre = { version = "0.6", features = ["capture-spantrace"], optional = true } http = "1.0.0" diff --git a/README.md b/README.md index dcecfce..98d9392 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,9 @@ -# `kinode_process_lib` +# `hyperware_process_lib` -Library of functions for more ergonomic [Kinode](https://github.com/kinode-dao/kinode) Rust process development. +Library of functions for more ergonomic Hyperware Rust process development. -[Documentation can be found here](https://docs.rs/kinode_process_lib). +[Documentation can be found here](https://docs.rs/hyperware_process_lib). -[Crate can be found here](https://crates.io/crates/kinode_process_lib). +[Crate can be found here](https://crates.io/crates/hyperware_process_lib). -See the [Kinode Book](https://book.kinode.org) for a guide on how to use this library to write Kinode apps in Rust. - -The major version of `kinode_process_lib` will always match the major version of Kinode OS. -Since the current major version of both is 0, breaking changes can occur on minor releases. -Once the major version reaches 1, breaking changes will only occur between major versions. -As is, developers may have to update their version of `process_lib` as they update Kinode OS. +See the [Hyperware Book](https://book.hyperware.ai) for a guide on how to use this library to write Hyperware apps in Rust. diff --git a/hyperware-wit/hypermap-cacher-sys-v0.wit b/hyperware-wit/hypermap-cacher-sys-v0.wit new file mode 100644 index 0000000..21a3652 --- /dev/null +++ b/hyperware-wit/hypermap-cacher-sys-v0.wit @@ -0,0 +1,83 @@ +interface hypermap-cacher { + // Metadata associated with a batch of Ethereum logs. + record logs-metadata { + chain-id: string, + from-block: string, + to-block: string, + time-created: string, + created-by: string, + signature: string, + } + + // Represents an item in the manifest, detailing a single log cache file. + record manifest-item { + metadata: logs-metadata, + is-empty: bool, + file-hash: string, + file-name: string, + } + + // The main manifest structure, listing all available log cache files. + // WIT does not support direct map types, so a list of key-value tuples is used. + record manifest { + // The key is the filename of the log cache. + items: list>, + manifest-filename: string, + chain-id: string, + protocol-version: string, + } + + record get-logs-by-range-request { + from-block: u64, + to-block: option, // If None, signifies to the latest available/relevant cached block. + } + + variant get-logs-by-range-ok-response { + logs(tuple), + latest(u64), + } + + // Defines the types of requests that can be sent to the Hypermap Cacher process. + variant cacher-request { + get-manifest, + get-log-cache-content(string), + get-status, + get-logs-by-range(get-logs-by-range-request), + start-providing, + stop-providing, + set-nodes(list), + reset(option>), + } + + // Represents the operational status of the cacher. + record cacher-status { + last-cached-block: u64, + chain-id: string, + protocol-version: string, + next-cache-attempt-in-seconds: option, + manifest-filename: string, + log-files-count: u32, + our-address: string, + is-providing: bool, + } + + // Defines the types of responses the Hypermap Cacher process can send. + variant cacher-response { + get-manifest(option), + get-log-cache-content(result, string>), + get-status(cacher-status), + get-logs-by-range(result), + start-providing(result), + stop-providing(result), + set-nodes(result), + reset(result), + rejected, + is-starting, + } +} + +world hypermap-cacher-sys-v0 { + import sign; + import hypermap-cacher; + include process-v1; +} diff --git a/kinode-wit/kinode.wit b/hyperware-wit/hyperware.wit similarity index 95% rename from kinode-wit/kinode.wit rename to hyperware-wit/hyperware.wit index adbfd97..6b4bf97 100644 --- a/kinode-wit/kinode.wit +++ b/hyperware-wit/hyperware.wit @@ -1,9 +1,4 @@ -// -// kinode.wit, copied from https://github.com/kinode-dao/kinode-wit -// https://raw.githubusercontent.com/kinode-dao/kinode-wit/v1.0.0/kinode.wit -// - -package kinode:process@1.0.0; +package hyperware:process@1.0.0; interface standard { @@ -14,7 +9,7 @@ interface standard { /// JSON is passed over Wasm boundary as a string. type json = string; - /// In types passed from kernel, node-id will be a valid Kimap entry. + /// In types passed from kernel, node-id will be a valid Hypermap entry. type node-id = string; /// Context, like a message body, is a protocol-defined serialized byte @@ -49,7 +44,7 @@ interface standard { // expects-response is none, direct response to source of parent. // also carries forward certain aspects of parent message in kernel, // see documentation for formal spec and examples: - // https://docs.rs/kinode_process_lib/latest/kinode_process_lib/struct.Request.html + // https://docs.rs/hyperware_process_lib/latest/hyperware_process_lib/struct.Request.html inherit: bool, // if some, request expects a response in the given number of seconds expects-response: option, @@ -226,4 +221,4 @@ world process-v1 { include lib; export init: func(our: string); -} \ No newline at end of file +} diff --git a/hyperware-wit/process-lib.wit b/hyperware-wit/process-lib.wit new file mode 100644 index 0000000..834f96b --- /dev/null +++ b/hyperware-wit/process-lib.wit @@ -0,0 +1,5 @@ +world process-lib { + import sign; + import hypermap-cacher; + include lib; +} diff --git a/hyperware-wit/sign-sys-v0.wit b/hyperware-wit/sign-sys-v0.wit new file mode 100644 index 0000000..9d89bab --- /dev/null +++ b/hyperware-wit/sign-sys-v0.wit @@ -0,0 +1,61 @@ +interface sign { + use standard.{address}; + + variant request { + /// Request to sign the message given in blob with net key. + /// + /// lazy-load-blob: required; the message to sign. + net-key-sign, + /// Request to verify the message given in blob with net key. + /// + /// lazy-load-blob: required; the message to verify. + net-key-verify(net-key-verify-request), + /// Request to transform the message to the form that is signed with net key. + /// For use by outside verifiers (net-key-verify transforms naked message + /// properly under-the-hood). + /// + /// lazy-load-blob: required; the message to transform. + net-key-make-message, + } + + variant response { + /// Response containing the net key signature in blob. + /// The source (address) will always be prepended to the payload. + /// The source (address) of sign:sign:sys will also be prepended. + /// Thus the message signed looks like + /// [sign-address, address, blob.bytes].concat() + /// + /// Using request::net-key-verify handles the concatenation under-the-hood, + /// but verifying the signature will require the proper transformation of + /// the message. + /// + /// lazy-load-blob: required; signature. + net-key-sign, + /// Response: whether the net key signature is valid. + /// + /// lazy-load-blob: none. + net-key-verify(bool), + /// Response containing modified message in blob. + /// The source (address) will always be prepended to the payload. + /// The source (address) of sign:sign:sys will also be prepended. + /// Thus the message signed looks like + /// [sign-address, address, blob.bytes].concat() + /// + /// Using request::net-key-verify handles the concatenation under-the-hood, + /// but verifying the signature will require the proper transformation of + /// the message. + /// + /// lazy-load-blob: required; the transformed message. + net-key-make-message, + } + + record net-key-verify-request { + node: string, + signature: list, + } +} + +world sign-sys-v0 { + import sign; + include process-v1; +} diff --git a/pull_request_template.md b/pull_request_template.md index fff18e8..1b76ef5 100644 --- a/pull_request_template.md +++ b/pull_request_template.md @@ -8,7 +8,7 @@ ## Docs Update -[Corresponding docs PR](https://github.com/kinode-dao/kinode-book/pull/my-pr-number) +[Corresponding docs PR](https://github.com/hyperware-ai/hyperware-book/pull/my-pr-number) ## Notes diff --git a/src/eth.rs b/src/eth.rs index 16f3a1a..19395fb 100644 --- a/src/eth.rs +++ b/src/eth.rs @@ -7,6 +7,7 @@ pub use alloy::rpc::types::{ Block, BlockId, BlockNumberOrTag, FeeHistory, Filter, FilterBlockOption, Log, Transaction, TransactionReceipt, }; +pub use alloy::transports::Authorization as AlloyAuthorization; pub use alloy_primitives::{Address, BlockHash, BlockNumber, Bytes, TxHash, U128, U256, U64, U8}; use serde::{Deserialize, Serialize}; use std::collections::{HashMap, HashSet}; @@ -67,7 +68,7 @@ pub enum EthAction { }, /// Kill a SubscribeLogs subscription of a given ID, to stop getting updates. UnsubscribeLogs(u64), - /// Raw request. Used by kinode_process_lib. + /// Raw request. Used by hyperware_process_lib. Request { chain_id: u64, method: String, @@ -187,7 +188,7 @@ pub enum EthConfigAction { pub enum EthConfigResponse { Ok, /// Response from a GetProviders request. - /// Note the [`crate::net::KnsUpdate`] will only have the correct `name` field. + /// Note the [`crate::net::HnsUpdate`] will only have the correct `name` field. /// The rest of the Update is not saved in this module. Providers(SavedConfigs), /// Response from a GetAccessSettings request. @@ -220,20 +221,82 @@ pub struct ProviderConfig { } #[derive(Clone, Debug, Deserialize, Serialize, Hash, Eq, PartialEq)] +pub enum Authorization { + Basic(String), + Bearer(String), + Raw(String), +} + +impl From for AlloyAuthorization { + fn from(auth: Authorization) -> AlloyAuthorization { + match auth { + Authorization::Basic(value) => AlloyAuthorization::Basic(value), + Authorization::Bearer(value) => AlloyAuthorization::Bearer(value), + Authorization::Raw(value) => AlloyAuthorization::Raw(value), + } + } +} + +#[derive(Clone, Debug, Serialize, Hash, Eq, PartialEq)] pub enum NodeOrRpcUrl { Node { - kns_update: crate::net::KnsUpdate, + hns_update: crate::net::HnsUpdate, use_as_provider: bool, // false for just-routers inside saved config }, - RpcUrl(String), + RpcUrl { + url: String, + auth: Option, + }, } impl std::cmp::PartialEq for NodeOrRpcUrl { fn eq(&self, other: &str) -> bool { match self { - NodeOrRpcUrl::Node { kns_update, .. } => kns_update.name == other, - NodeOrRpcUrl::RpcUrl(url) => url == other, + NodeOrRpcUrl::Node { hns_update, .. } => hns_update.name == other, + NodeOrRpcUrl::RpcUrl { url, .. } => url == other, + } + } +} + +impl<'de> Deserialize<'de> for NodeOrRpcUrl { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + #[derive(Deserialize)] + #[serde(untagged)] + enum RpcUrlHelper { + String(String), + Struct { + url: String, + auth: Option, + }, + } + + #[derive(Deserialize)] + enum Helper { + Node { + hns_update: crate::net::HnsUpdate, + use_as_provider: bool, + }, + RpcUrl(RpcUrlHelper), } + + let helper = Helper::deserialize(deserializer)?; + + Ok(match helper { + Helper::Node { + hns_update, + use_as_provider, + } => NodeOrRpcUrl::Node { + hns_update, + use_as_provider, + }, + Helper::RpcUrl(url_helper) => match url_helper { + RpcUrlHelper::String(url) => NodeOrRpcUrl::RpcUrl { url, auth: None }, + RpcUrlHelper::Struct { url, auth } => NodeOrRpcUrl::RpcUrl { url, auth }, + }, + }) } } @@ -255,6 +318,11 @@ impl Provider { request_timeout, } } + + pub fn get_chain_id(&self) -> u64 { + self.chain_id + } + /// Sends a request based on the specified [`EthAction`] and parses the response. /// /// This function constructs a request targeting the Ethereum distribution system, serializes the provided [`EthAction`], @@ -612,14 +680,14 @@ impl Provider { self.send_request_and_parse_response::(action) } - /// Returns a Kimap instance with the default address using this provider. - pub fn kimap(&self) -> crate::kimap::Kimap { - crate::kimap::Kimap::default(self.request_timeout) + /// Returns a Hypermap instance with the default address using this provider. + pub fn hypermap(&self) -> crate::hypermap::Hypermap { + crate::hypermap::Hypermap::default(self.request_timeout) } - /// Returns a Kimap instance with a custom address using this provider. - pub fn kimap_with_address(self, address: Address) -> crate::kimap::Kimap { - crate::kimap::Kimap::new(self, address) + /// Returns a Hypermap instance with a custom address using this provider. + pub fn hypermap_with_address(self, address: Address) -> crate::hypermap::Hypermap { + crate::hypermap::Hypermap::new(self, address) } /// Sends a raw transaction to the network. diff --git a/src/homepage.rs b/src/homepage.rs index c4d2879..ad963b9 100644 --- a/src/homepage.rs +++ b/src/homepage.rs @@ -1,6 +1,6 @@ use crate::Request; -/// Add a new icon and/or widget to the Kinode homepage. Note that the process calling this +/// Add a new icon and/or widget to the Hyperware homepage. Note that the process calling this /// function must have the `homepage:homepage:sys` messaging [`crate::Capability`]. /// /// This should be called upon process startup to ensure that the process is added to the homepage. @@ -29,7 +29,7 @@ pub fn add_to_homepage(label: &str, icon: Option<&str>, path: Option<&str>, widg .unwrap(); } -/// Remove the caller process from the Kinode homepage. Note that the process calling this function +/// Remove the caller process from the Hyperware homepage. Note that the process calling this function /// must have the `homepage:homepage:sys` messaging [`crate::Capability`]. /// /// This usually isn't necessary as processes are not persisted on homepage between boots. diff --git a/src/http/server.rs b/src/http/server.rs index 26a118f..c4eb230 100644 --- a/src/http/server.rs +++ b/src/http/server.rs @@ -202,14 +202,14 @@ pub enum HttpServerAction { }, /// When sent, expects a [`crate::LazyLoadBlob`] containing the WebSocket message bytes to send. /// Modifies the [`crate::LazyLoadBlob`] by placing into [`HttpServerAction::WebSocketExtPushData`]` with id taken from - /// this [`KernelMessage`]` and `kinode_message_type` set to `desired_reply_type`. + /// this [`KernelMessage`]` and `hyperware_message_type` set to `desired_reply_type`. WebSocketExtPushOutgoing { channel_id: u32, message_type: WsMessageType, desired_reply_type: MessageType, }, /// For communicating with the ext. - /// Kinode's http-server sends this to the ext after receiving [`HttpServerAction::WebSocketExtPushOutgoing`]. + /// Hyperware's http-server sends this to the ext after receiving [`HttpServerAction::WebSocketExtPushOutgoing`]. /// Upon receiving reply with this type from ext, http-server parses, setting: /// * id as given, /// * message type as given ([`crate::Request`] or [`crate::Response`]), @@ -217,7 +217,7 @@ pub enum HttpServerAction { /// * [`crate::LazyLoadBlob`] as given. WebSocketExtPushData { id: u64, - kinode_message_type: MessageType, + hyperware_message_type: MessageType, blob: Vec, }, /// Sending will close a socket the process controls. @@ -562,7 +562,7 @@ impl HttpServer { }) .unwrap(), ) - .blob(crate::kinode::process::standard::LazyLoadBlob { + .blob(crate::hyperware::process::standard::LazyLoadBlob { mime: content_type.clone(), bytes: content.clone(), }) @@ -597,7 +597,7 @@ impl HttpServer { /// Instead of binding at just a path, this function tells the HTTP server to /// generate a *subdomain* with our package ID (with non-ascii-alphanumeric /// characters converted to `-`, although will not be needed if package ID is - /// a genuine kimap entry) and bind at that subdomain. + /// a genuine hypermap entry) and bind at that subdomain. pub fn secure_bind_http_path(&mut self, path: T) -> Result<(), HttpServerError> where T: Into, @@ -639,7 +639,7 @@ impl HttpServer { /// Instead of binding at just a path, this function tells the HTTP server to /// generate a *subdomain* with our package ID (with non-ascii-alphanumeric /// characters converted to `-`, although will not be needed if package ID is - /// a genuine kimap entry) and bind at that subdomain. + /// a genuine hypermap entry) and bind at that subdomain. pub fn secure_bind_ws_path(&mut self, path: T) -> Result<(), HttpServerError> where T: Into, diff --git a/src/hypermap.rs b/src/hypermap.rs new file mode 100644 index 0000000..eb09967 --- /dev/null +++ b/src/hypermap.rs @@ -0,0 +1,1976 @@ +use crate::eth::{ + BlockNumberOrTag, EthError, Filter as EthFilter, FilterBlockOption, Log as EthLog, Provider, +}; +use crate::hypermap::contract::getCall; +use crate::hyperware::process::hypermap_cacher::{ + CacherRequest, CacherResponse, CacherStatus, GetLogsByRangeOkResponse, GetLogsByRangeRequest, + LogsMetadata, Manifest, ManifestItem, +}; +use crate::{net, sign}; +use crate::{print_to_terminal, Address as HyperAddress, Request}; +use alloy::hex; +use alloy::rpc::types::request::{TransactionInput, TransactionRequest}; +use alloy_primitives::{keccak256, Address, Bytes, FixedBytes, B256}; +use alloy_sol_types::{SolCall, SolEvent, SolValue}; +use contract::tokenCall; +use serde::{ + self, + de::{self, MapAccess, Visitor}, + ser::{SerializeMap, SerializeStruct}, + Deserialize, Deserializer, Serialize, Serializer, +}; +use std::error::Error; +use std::fmt; +use std::str::FromStr; + +/// hypermap deployment address on base +pub const HYPERMAP_ADDRESS: &'static str = "0x000000000044C6B8Cb4d8f0F889a3E47664EAeda"; +/// base chain id +#[cfg(not(feature = "simulation-mode"))] +pub const HYPERMAP_CHAIN_ID: u64 = 8453; // base +#[cfg(feature = "simulation-mode")] +pub const HYPERMAP_CHAIN_ID: u64 = 31337; // fakenet +/// first block (minus one) of hypermap deployment on base +#[cfg(not(feature = "simulation-mode"))] +pub const HYPERMAP_FIRST_BLOCK: u64 = 27_270_411; +#[cfg(feature = "simulation-mode")] +pub const HYPERMAP_FIRST_BLOCK: u64 = 0; +/// the root hash of hypermap, empty bytes32 +pub const HYPERMAP_ROOT_HASH: &'static str = + "0x0000000000000000000000000000000000000000000000000000000000000000"; + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct LogCache { + pub metadata: LogsMetadata, + pub logs: Vec, +} + +const CACHER_REQUEST_TIMEOUT_S: u64 = 15; + +/// Sol structures for Hypermap requests +pub mod contract { + use alloy_sol_macro::sol; + + sol! { + /// Emitted when a new namespace entry is minted. + /// - parenthash: The hash of the parent namespace entry. + /// - childhash: The hash of the minted namespace entry's full path. + /// - labelhash: The hash of only the label (the final entry in the path). + /// - label: The label (the final entry in the path) of the new entry. + event Mint( + bytes32 indexed parenthash, + bytes32 indexed childhash, + bytes indexed labelhash, + bytes label + ); + + /// Emitted when a fact is created on an existing namespace entry. + /// Facts are immutable and may only be written once. A fact label is + /// prepended with an exclamation mark (!) to indicate that it is a fact. + /// - parenthash The hash of the parent namespace entry. + /// - facthash The hash of the newly created fact's full path. + /// - labelhash The hash of only the label (the final entry in the path). + /// - label The label of the fact. + /// - data The data stored at the fact. + event Fact( + bytes32 indexed parenthash, + bytes32 indexed facthash, + bytes indexed labelhash, + bytes label, + bytes data + ); + + /// Emitted when a new note is created on an existing namespace entry. + /// Notes are mutable. A note label is prepended with a tilde (~) to indicate + /// that it is a note. + /// - parenthash: The hash of the parent namespace entry. + /// - notehash: The hash of the newly created note's full path. + /// - labelhash: The hash of only the label (the final entry in the path). + /// - label: The label of the note. + /// - data: The data stored at the note. + event Note( + bytes32 indexed parenthash, + bytes32 indexed notehash, + bytes indexed labelhash, + bytes label, + bytes data + ); + + /// Emitted when a gene is set for an existing namespace entry. + /// A gene is a specific TBA implementation which will be applied to all + /// sub-entries of the namespace entry. + /// - entry: The namespace entry's namehash. + /// - gene: The address of the TBA implementation. + event Gene(bytes32 indexed entry, address indexed gene); + + /// Emitted when the zeroth namespace entry is minted. + /// Occurs exactly once at initialization. + /// - zeroTba: The address of the zeroth TBA + event Zero(address indexed zeroTba); + + /// Emitted when a namespace entry is transferred from one address + /// to another. + /// - from: The address of the sender. + /// - to: The address of the recipient. + /// - id: The namehash of the namespace entry (converted to uint256). + event Transfer( + address indexed from, + address indexed to, + uint256 indexed id + ); + + /// Emitted when a namespace entry is approved for transfer. + /// - owner: The address of the owner. + /// - spender: The address of the spender. + /// - id: The namehash of the namespace entry (converted to uint256). + event Approval( + address indexed owner, + address indexed spender, + uint256 indexed id + ); + + /// Emitted when an operator is approved for all of an owner's + /// namespace entries. + /// - owner: The address of the owner. + /// - operator: The address of the operator. + /// - approved: Whether the operator is approved. + event ApprovalForAll( + address indexed owner, + address indexed operator, + bool approved + ); + + /// Retrieves information about a specific namespace entry. + /// - namehash The namehash of the namespace entry to query. + /// + /// Returns: + /// - tba: The address of the token-bound account associated + /// with the entry. + /// - owner: The address of the entry owner. + /// - data: The note or fact bytes associated with the entry + /// (empty if not a note or fact). + function get( + bytes32 namehash + ) external view returns (address tba, address owner, bytes memory data); + + /// Mints a new namespace entry and creates a token-bound account for + /// it. Must be called by a parent namespace entry token-bound account. + /// - who: The address to own the new namespace entry. + /// - label: The label to mint beneath the calling parent entry. + /// - initialization: Initialization calldata applied to the new + /// minted entry's token-bound account. + /// - erc721Data: ERC-721 data -- passed to comply with + /// `ERC721TokenReceiver.onERC721Received()`. + /// - implementation: The address of the implementation contract for + /// the token-bound account: this will be overriden by the gene if the + /// parent entry has one set. + /// + /// Returns: + /// - tba: The address of the new entry's token-bound account. + function mint( + address who, + bytes calldata label, + bytes calldata initialization, + bytes calldata erc721Data, + address implementation + ) external returns (address tba); + + /// Sets the gene for the calling namespace entry. + /// - _gene: The address of the TBA implementation to set for all + /// children of the calling namespace entry. + function gene(address _gene) external; + + /// Creates a new fact beneath the calling namespace entry. + /// - fact: The fact label to create. Must be prepended with an + /// exclamation mark (!). + /// - data: The data to be stored at the fact. + /// + /// Returns: + /// - facthash: The namehash of the newly created fact. + function fact( + bytes calldata fact, + bytes calldata data + ) external returns (bytes32 facthash); + + /// Creates a new note beneath the calling namespace entry. + /// - note: The note label to create. Must be prepended with a tilde (~). + /// - data: The data to be stored at the note. + /// + /// Returns: + /// - notehash: The namehash of the newly created note. + function note( + bytes calldata note, + bytes calldata data + ) external returns (bytes32 notehash); + + /// Retrieves the token-bound account address of a namespace entry. + /// - entry: The entry namehash (as uint256) for which to get the + /// token-bound account. + /// + /// Returns: + /// - tba: The token-bound account address of the namespace entry. + function tbaOf(uint256 entry) external view returns (address tba); + + function balanceOf(address owner) external view returns (uint256); + + function getApproved(uint256 entry) external view returns (address); + + function isApprovedForAll( + address owner, + address operator + ) external view returns (bool); + + function ownerOf(uint256 entry) external view returns (address); + + function setApprovalForAll(address operator, bool approved) external; + + function approve(address spender, uint256 entry) external; + + function safeTransferFrom(address from, address to, uint256 id) external; + + function safeTransferFrom( + address from, + address to, + uint256 id, + bytes calldata data + ) external; + + function transferFrom(address from, address to, uint256 id) external; + + function supportsInterface(bytes4 interfaceId) external view returns (bool); + + /// Gets the token identifier that owns this token-bound account (TBA). + /// This is a core function of the ERC-6551 standard that returns the + /// identifying information about the NFT that owns this account. + /// The return values are constant and cannot change over time. + /// + /// Returns: + /// - chainId: The EIP-155 chain ID where the owning NFT exists + /// - tokenContract: The contract address of the owning NFT + /// - tokenId: The token ID of the owning NFT + function token() + external + view + returns (uint256 chainId, address tokenContract, uint256 tokenId); + } +} + +/// A mint log from the hypermap, converted to a 'resolved' format using +/// namespace data saved in the hns-indexer. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Mint { + pub name: String, + pub parent_path: String, +} + +/// A note log from the hypermap, converted to a 'resolved' format using +/// namespace data saved in the hns-indexer +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Note { + pub note: String, + pub parent_path: String, + pub data: Bytes, +} + +/// A fact log from the hypermap, converted to a 'resolved' format using +/// namespace data saved in the hns-indexer +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Fact { + pub fact: String, + pub parent_path: String, + pub data: Bytes, +} + +/// Errors that can occur when decoding a log from the hypermap using +/// [`decode_mint_log()`] or [`decode_note_log()`]. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub enum DecodeLogError { + /// The log's topic is not a mint or note event. + UnexpectedTopic(B256), + /// The name is not valid (according to [`valid_name`]). + InvalidName(String), + /// An error occurred while decoding the log. + DecodeError(String), + /// The parent name could not be resolved with `hns-indexer`. + UnresolvedParent(String), +} + +impl fmt::Display for DecodeLogError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + DecodeLogError::UnexpectedTopic(topic) => write!(f, "Unexpected topic: {:?}", topic), + DecodeLogError::InvalidName(name) => write!(f, "Invalid name: {}", name), + DecodeLogError::DecodeError(err) => write!(f, "Decode error: {}", err), + DecodeLogError::UnresolvedParent(parent) => { + write!(f, "Could not resolve parent: {}", parent) + } + } + } +} + +impl Error for DecodeLogError {} + +/// Canonical function to determine if a hypermap entry is valid. +/// +/// This checks a **single name**, not the full path-name. A full path-name +/// is comprised of valid names separated by `.` +pub fn valid_entry(entry: &str, note: bool, fact: bool) -> bool { + if note && fact { + return false; + } + if note { + valid_note(entry) + } else if fact { + valid_fact(entry) + } else { + valid_name(entry) + } +} + +pub fn valid_name(name: &str) -> bool { + name.is_ascii() + && name.len() >= 1 + && name + .chars() + .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-') +} + +pub fn valid_note(note: &str) -> bool { + note.is_ascii() + && note.len() >= 2 + && note.chars().next() == Some('~') + && note + .chars() + .skip(1) + .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-') +} + +pub fn valid_fact(fact: &str) -> bool { + fact.is_ascii() + && fact.len() >= 2 + && fact.chars().next() == Some('!') + && fact + .chars() + .skip(1) + .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-') +} + +/// Produce a namehash from a hypermap name. +pub fn namehash(name: &str) -> String { + let mut node = B256::default(); + + let mut labels: Vec<&str> = name.split('.').collect(); + labels.reverse(); + + for label in labels.iter() { + let l = keccak256(label); + node = keccak256((node, l).abi_encode_packed()); + } + format!("0x{}", hex::encode(node)) +} + +/// Decode a mint log from the hypermap into a 'resolved' format. +/// +/// Uses [`valid_name()`] to check if the name is valid. +pub fn decode_mint_log(log: &crate::eth::Log) -> Result { + let contract::Note::SIGNATURE_HASH = log.topics()[0] else { + return Err(DecodeLogError::UnexpectedTopic(log.topics()[0])); + }; + let decoded = contract::Mint::decode_log_data(log.data(), true) + .map_err(|e| DecodeLogError::DecodeError(e.to_string()))?; + let name = String::from_utf8_lossy(&decoded.label).to_string(); + if !valid_name(&name) { + return Err(DecodeLogError::InvalidName(name)); + } + match resolve_parent(log, None) { + Some(parent_path) => Ok(Mint { name, parent_path }), + None => Err(DecodeLogError::UnresolvedParent(name)), + } +} + +/// Decode a note log from the hypermap into a 'resolved' format. +/// +/// Uses [`valid_name()`] to check if the name is valid. +pub fn decode_note_log(log: &crate::eth::Log) -> Result { + let contract::Note::SIGNATURE_HASH = log.topics()[0] else { + return Err(DecodeLogError::UnexpectedTopic(log.topics()[0])); + }; + let decoded = contract::Note::decode_log_data(log.data(), true) + .map_err(|e| DecodeLogError::DecodeError(e.to_string()))?; + let note = String::from_utf8_lossy(&decoded.label).to_string(); + if !valid_note(¬e) { + return Err(DecodeLogError::InvalidName(note)); + } + match resolve_parent(log, None) { + Some(parent_path) => Ok(Note { + note, + parent_path, + data: decoded.data, + }), + None => Err(DecodeLogError::UnresolvedParent(note)), + } +} + +pub fn decode_fact_log(log: &crate::eth::Log) -> Result { + let contract::Fact::SIGNATURE_HASH = log.topics()[0] else { + return Err(DecodeLogError::UnexpectedTopic(log.topics()[0])); + }; + let decoded = contract::Fact::decode_log_data(log.data(), true) + .map_err(|e| DecodeLogError::DecodeError(e.to_string()))?; + let fact = String::from_utf8_lossy(&decoded.label).to_string(); + if !valid_fact(&fact) { + return Err(DecodeLogError::InvalidName(fact)); + } + match resolve_parent(log, None) { + Some(parent_path) => Ok(Fact { + fact, + parent_path, + data: decoded.data, + }), + None => Err(DecodeLogError::UnresolvedParent(fact)), + } +} + +/// Given a [`crate::eth::Log`] (which must be a log from hypermap), resolve the parent name +/// of the new entry or note. +pub fn resolve_parent(log: &crate::eth::Log, timeout: Option) -> Option { + let parent_hash = log.topics()[1].to_string(); + net::get_name(&parent_hash, log.block_number, timeout) +} + +/// Given a [`crate::eth::Log`] (which must be a log from hypermap), resolve the full name +/// of the new entry or note. +/// +/// Uses [`valid_name()`] to check if the name is valid. +pub fn resolve_full_name(log: &crate::eth::Log, timeout: Option) -> Option { + let parent_hash = log.topics()[1].to_string(); + let parent_name = net::get_name(&parent_hash, log.block_number, timeout)?; + let log_name = match log.topics()[0] { + contract::Mint::SIGNATURE_HASH => { + let decoded = contract::Mint::decode_log_data(log.data(), true).unwrap(); + decoded.label + } + contract::Note::SIGNATURE_HASH => { + let decoded = contract::Note::decode_log_data(log.data(), true).unwrap(); + decoded.label + } + contract::Fact::SIGNATURE_HASH => { + let decoded = contract::Fact::decode_log_data(log.data(), true).unwrap(); + decoded.label + } + _ => return None, + }; + let name = String::from_utf8_lossy(&log_name); + if !valid_entry( + &name, + log.topics()[0] == contract::Note::SIGNATURE_HASH, + log.topics()[0] == contract::Fact::SIGNATURE_HASH, + ) { + return None; + } + Some(format!("{name}.{parent_name}")) +} + +pub fn eth_apply_filter(logs: &[EthLog], filter: &EthFilter) -> Vec { + let mut matched_logs = Vec::new(); + + let (filter_from_block, filter_to_block) = match filter.block_option { + FilterBlockOption::Range { + from_block, + to_block, + } => { + let parse_block_num = |bn: Option| -> Option { + match bn { + Some(BlockNumberOrTag::Number(n)) => Some(n), + _ => None, + } + }; + (parse_block_num(from_block), parse_block_num(to_block)) + } + _ => (None, None), + }; + + for log in logs.iter() { + let mut match_address = filter.address.is_empty(); + if !match_address { + if filter.address.matches(&log.address()) { + match_address = true; + } + } + if !match_address { + continue; + } + + if let Some(log_bn) = log.block_number { + if let Some(filter_from) = filter_from_block { + if log_bn < filter_from { + continue; + } + } + if let Some(filter_to) = filter_to_block { + if log_bn > filter_to { + continue; + } + } + } else { + if filter_from_block.is_some() || filter_to_block.is_some() { + continue; + } + } + + let mut match_topics = true; + for (i, filter_topic_alternatives) in filter.topics.iter().enumerate() { + if filter_topic_alternatives.is_empty() { + continue; + } + + let log_topic = log.topics().get(i); + let mut current_topic_matched = false; + for filter_topic in filter_topic_alternatives.iter() { + if log_topic == Some(filter_topic) { + current_topic_matched = true; + break; + } + } + if !current_topic_matched { + match_topics = false; + break; + } + } + + if match_topics { + matched_logs.push(log.clone()); + } + } + matched_logs +} + +/// Helper struct for reading from the hypermap. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Hypermap { + pub provider: Provider, + address: Address, +} + +impl Hypermap { + /// Creates a new Hypermap instance with a specified address. + /// + /// # Arguments + /// * `provider` - A reference to the Provider. + /// * `address` - The address of the Hypermap contract. + pub fn new(provider: Provider, address: Address) -> Self { + Self { provider, address } + } + + /// Creates a new Hypermap instance with the default address and chain ID. + pub fn default(timeout: u64) -> Self { + let provider = Provider::new(HYPERMAP_CHAIN_ID, timeout); + Self::new(provider, Address::from_str(HYPERMAP_ADDRESS).unwrap()) + } + + /// Returns the in-use Hypermap contract address. + pub fn address(&self) -> &Address { + &self.address + } + + /// Gets an entry from the Hypermap by its string-formatted name. + /// + /// # Parameters + /// - `path`: The name-path to get from the Hypermap. + /// # Returns + /// A `Result<(Address, Address, Option), EthError>` representing the TBA, owner, + /// and value if the entry exists and is a note. + pub fn get(&self, path: &str) -> Result<(Address, Address, Option), EthError> { + let get_call = getCall { + namehash: FixedBytes::<32>::from_str(&namehash(path)) + .map_err(|_| EthError::InvalidParams)?, + } + .abi_encode(); + + let tx_req = TransactionRequest::default() + .input(TransactionInput::new(get_call.into())) + .to(self.address); + + let res_bytes = self.provider.call(tx_req, None)?; + + let res = getCall::abi_decode_returns(&res_bytes, false) + .map_err(|_| EthError::RpcMalformedResponse)?; + + let note_data = if res.data == Bytes::default() { + None + } else { + Some(res.data) + }; + + Ok((res.tba, res.owner, note_data)) + } + + /// Gets an entry from the Hypermap by its hash. + /// + /// # Parameters + /// - `entryhash`: The entry to get from the Hypermap. + /// # Returns + /// A `Result<(Address, Address, Option), EthError>` representing the TBA, owner, + /// and value if the entry exists and is a note. + pub fn get_hash(&self, entryhash: &str) -> Result<(Address, Address, Option), EthError> { + let get_call = getCall { + namehash: FixedBytes::<32>::from_str(entryhash).map_err(|_| EthError::InvalidParams)?, + } + .abi_encode(); + + let tx_req = TransactionRequest::default() + .input(TransactionInput::new(get_call.into())) + .to(self.address); + + let res_bytes = self.provider.call(tx_req, None)?; + + let res = getCall::abi_decode_returns(&res_bytes, false) + .map_err(|_| EthError::RpcMalformedResponse)?; + + let note_data = if res.data == Bytes::default() { + None + } else { + Some(res.data) + }; + + Ok((res.tba, res.owner, note_data)) + } + + /// Gets a namehash from an existing TBA address. + /// + /// # Parameters + /// - `tba`: The TBA to get the namehash of. + /// # Returns + /// A `Result` representing the namehash of the TBA. + pub fn get_namehash_from_tba(&self, tba: Address) -> Result { + let token_call = tokenCall {}.abi_encode(); + + let tx_req = TransactionRequest::default() + .input(TransactionInput::new(token_call.into())) + .to(tba); + + let res_bytes = self.provider.call(tx_req, None)?; + + let res = tokenCall::abi_decode_returns(&res_bytes, false) + .map_err(|_| EthError::RpcMalformedResponse)?; + + let namehash: FixedBytes<32> = res.tokenId.into(); + Ok(format!("0x{}", hex::encode(namehash))) + } + + /// Create a filter for all mint events. + pub fn mint_filter(&self) -> crate::eth::Filter { + crate::eth::Filter::new() + .address(self.address) + .event(contract::Mint::SIGNATURE) + } + + /// Create a filter for all note events. + pub fn note_filter(&self) -> crate::eth::Filter { + crate::eth::Filter::new() + .address(self.address) + .event(contract::Note::SIGNATURE) + } + + /// Create a filter for all fact events. + pub fn fact_filter(&self) -> crate::eth::Filter { + crate::eth::Filter::new() + .address(self.address) + .event(contract::Fact::SIGNATURE) + } + + /// Create a filter for a given set of specific notes. This function will + /// hash the note labels and use them as the topic3 filter. + /// + /// Example: + /// ```rust + /// let filter = hypermap.notes_filter(&["~note1", "~note2"]); + /// ``` + pub fn notes_filter(&self, notes: &[&str]) -> crate::eth::Filter { + self.note_filter().topic3( + notes + .into_iter() + .map(|note| keccak256(note)) + .collect::>(), + ) + } + + /// Create a filter for a given set of specific facts. This function will + /// hash the fact labels and use them as the topic3 filter. + /// + /// Example: + /// ```rust + /// let filter = hypermap.facts_filter(&["!fact1", "!fact2"]); + /// ``` + pub fn facts_filter(&self, facts: &[&str]) -> crate::eth::Filter { + self.fact_filter().topic3( + facts + .into_iter() + .map(|fact| keccak256(fact)) + .collect::>(), + ) + } + + fn get_bootstrap_log_cache_inner( + &self, + cacher_request: &CacherRequest, + cacher_process_address: &HyperAddress, + attempt: u64, + request_from_block_val: u64, + retry_delay_s: u64, + retry_count: Option, + chain: &Option, + ) -> anyhow::Result)>> { + let retry_count_str = retry_count + .map(|r| r.to_string()) + .unwrap_or_else(|| "inf".to_string()); + print_to_terminal( + 2, + &format!("Attempt {attempt}/{retry_count_str} to query local hypermap-cacher"), + ); + + let response_msg = match Request::to(cacher_process_address.clone()) + .body(serde_json::to_vec(cacher_request)?) + .send_and_await_response(CACHER_REQUEST_TIMEOUT_S) + { + Ok(Ok(msg)) => msg, + Ok(Err(e)) => { + print_to_terminal( + 1, + &format!( + "Error response from local cacher (attempt {}): {:?}", + attempt, e + ), + ); + if retry_count.is_none() || attempt < retry_count.unwrap() { + std::thread::sleep(std::time::Duration::from_secs(retry_delay_s)); + return Ok(None); + } else { + return Err(anyhow::anyhow!( + "Error response from local cacher after {retry_count_str} attempts: {e:?}" + )); + } + } + Err(e) => { + print_to_terminal( + 1, + &format!( + "Failed to send request to local cacher (attempt {}): {:?}", + attempt, e + ), + ); + if retry_count.is_none() || attempt < retry_count.unwrap() { + std::thread::sleep(std::time::Duration::from_secs(retry_delay_s)); + return Ok(None); + } else { + return Err(anyhow::anyhow!( + "Failed to send request to local cacher after {retry_count_str} attempts: {e:?}" + )); + } + } + }; + + match serde_json::from_slice::(response_msg.body())? { + CacherResponse::GetLogsByRange(res) => { + match res { + Ok(GetLogsByRangeOkResponse::Latest(block)) => { + return Ok(Some((block, vec![]))); + } + Ok(GetLogsByRangeOkResponse::Logs((block, json_string_of_vec_log_cache))) => { + if json_string_of_vec_log_cache.is_empty() + || json_string_of_vec_log_cache == "[]" + { + print_to_terminal( + 2, + &format!( + "Local cacher returned no log caches for the range from block {}.", + request_from_block_val, + ), + ); + return Ok(Some((block, vec![]))); + } + match serde_json::from_str::>(&json_string_of_vec_log_cache) { + Ok(retrieved_caches) => { + let target_chain_id = chain + .clone() + .unwrap_or_else(|| self.provider.get_chain_id().to_string()); + let mut filtered_caches = vec![]; + + for log_cache in retrieved_caches { + if log_cache.metadata.chain_id == target_chain_id { + // Further filter: ensure the cache's own from_block isn't completely after what we need, + // and to_block isn't completely before. + let cache_from = log_cache + .metadata + .from_block + .parse::() + .unwrap_or(u64::MAX); + let cache_to = + log_cache.metadata.to_block.parse::().unwrap_or(0); + + if cache_to >= request_from_block_val { + // Cache has some data at or after our request_from_block + filtered_caches.push(log_cache); + } else { + print_to_terminal(3, &format!("Cache from local cacher ({} to {}) does not meet request_from_block {}", + cache_from, cache_to, request_from_block_val)); + } + } else { + print_to_terminal(1,&format!("LogCache from local cacher has mismatched chain_id (expected {}, got {}). Skipping.", + target_chain_id, log_cache.metadata.chain_id)); + } + } + + print_to_terminal( + 2, + &format!( + "Retrieved {} log caches from local hypermap-cacher.", + filtered_caches.len(), + ), + ); + return Ok(Some((block, filtered_caches))); + } + Err(e) => { + return Err(anyhow::anyhow!( + "Failed to deserialize Vec from local cacher: {:?}. JSON: {:.100}", + e, json_string_of_vec_log_cache + )); + } + } + } + Err(e_str) => { + return Err(anyhow::anyhow!( + "Local cacher reported error for GetLogsByRange: {}", + e_str, + )); + } + } + } + CacherResponse::IsStarting => { + print_to_terminal( + 2, + &format!( + "Local hypermap-cacher is still starting (attempt {}/{}). Retrying in {}s...", + attempt, retry_count_str, retry_delay_s + ), + ); + if retry_count.is_none() || attempt < retry_count.unwrap() { + std::thread::sleep(std::time::Duration::from_secs(retry_delay_s)); + return Ok(None); + } else { + return Err(anyhow::anyhow!( + "Local hypermap-cacher is still starting after {retry_count_str} attempts" + )); + } + } + CacherResponse::Rejected => { + return Err(anyhow::anyhow!( + "Local hypermap-cacher rejected our request" + )); + } + _ => { + return Err(anyhow::anyhow!( + "Unexpected response type from local hypermap-cacher" + )); + } + } + } + + pub fn get_bootstrap_log_cache( + &self, + from_block: Option, + retry_params: Option<(u64, Option)>, + chain: Option, + ) -> anyhow::Result<(u64, Vec)> { + print_to_terminal(2, + &format!("get_bootstrap_log_cache (using local hypermap-cacher): from_block={:?}, retry_params={:?}, chain={:?}", + from_block, retry_params, chain) + ); + + let (retry_delay_s, retry_count) = retry_params.ok_or_else(|| { + anyhow::anyhow!("IsStarted check requires retry parameters (delay_s, max_tries)") + })?; + + let cacher_process_address = + HyperAddress::new("our", ("hypermap-cacher", "hypermap-cacher", "sys")); + + print_to_terminal( + 2, + &format!( + "Querying local cacher with GetLogsByRange: {}", + cacher_process_address.to_string(), + ), + ); + + let request_from_block_val = from_block.unwrap_or(0); + + let get_logs_by_range_payload = GetLogsByRangeRequest { + from_block: request_from_block_val, + to_block: None, // Request all logs from from_block onwards. Cacher will return what it has. + }; + let cacher_request = CacherRequest::GetLogsByRange(get_logs_by_range_payload); + + if let Some(retry_count) = retry_count { + for attempt in 1..=retry_count { + if let Some(return_vals) = self.get_bootstrap_log_cache_inner( + &cacher_request, + &cacher_process_address, + attempt, + request_from_block_val, + retry_delay_s, + Some(retry_count), + &chain, + )? { + return Ok(return_vals); + } + } + } else { + let mut attempt = 1; + loop { + if let Some(return_vals) = self.get_bootstrap_log_cache_inner( + &cacher_request, + &cacher_process_address, + attempt, + request_from_block_val, + retry_delay_s, + None, + &chain, + )? { + return Ok(return_vals); + } + attempt += 1; + } + } + + Err(anyhow::anyhow!( + "Failed to get response from local hypermap-cacher after {retry_count:?} attempts" + )) + } + + pub fn validate_log_cache(&self, log_cache: &LogCache) -> anyhow::Result { + let from_block = log_cache.metadata.from_block.parse::().map_err(|_| { + anyhow::anyhow!( + "Invalid from_block in metadata: {}", + log_cache.metadata.from_block + ) + })?; + let to_block = log_cache.metadata.to_block.parse::().map_err(|_| { + anyhow::anyhow!( + "Invalid to_block in metadata: {}", + log_cache.metadata.to_block + ) + })?; + + let mut bytes_to_verify = serde_json::to_vec(&log_cache.logs) + .map_err(|e| anyhow::anyhow!("Failed to serialize logs for validation: {:?}", e))?; + bytes_to_verify.extend_from_slice(&from_block.to_be_bytes()); + bytes_to_verify.extend_from_slice(&to_block.to_be_bytes()); + let hashed_data = keccak256(&bytes_to_verify); + + let signature_hex = log_cache.metadata.signature.trim_start_matches("0x"); + let signature_bytes = hex::decode(signature_hex) + .map_err(|e| anyhow::anyhow!("Failed to decode hex signature: {:?}", e))?; + + Ok(sign::net_key_verify( + hashed_data.to_vec(), + &log_cache.metadata.created_by.parse::()?, + signature_bytes, + )?) + } + + pub fn get_bootstrap( + &self, + from_block: Option, + retry_params: Option<(u64, Option)>, + chain: Option, + ) -> anyhow::Result<(u64, Vec)> { + print_to_terminal( + 2, + &format!( + "get_bootstrap: from_block={:?}, retry_params={:?}, chain={:?}", + from_block, retry_params, chain, + ), + ); + let (block, log_caches) = self.get_bootstrap_log_cache(from_block, retry_params, chain)?; + + let mut all_valid_logs: Vec = Vec::new(); + let request_from_block_val = from_block.unwrap_or(0); + + for log_cache in log_caches { + match self.validate_log_cache(&log_cache) { + Ok(true) => { + for log in log_cache.logs { + if let Some(log_block_number) = log.block_number { + if log_block_number >= request_from_block_val { + all_valid_logs.push(log); + } + } else { + if from_block.is_none() { + all_valid_logs.push(log); + } + } + } + } + Ok(false) => { + print_to_terminal( + 1, + &format!("LogCache validation failed for cache created by {}. Discarding {} logs.", + log_cache.metadata.created_by, + log_cache.logs.len()) + ); + } + Err(e) => { + print_to_terminal( + 1, + &format!( + "Error validating LogCache from {}: {:?}. Discarding.", + log_cache.metadata.created_by, e, + ), + ); + } + } + } + + all_valid_logs.sort_by(|a, b| { + let block_cmp = a.block_number.cmp(&b.block_number); + if block_cmp == std::cmp::Ordering::Equal { + std::cmp::Ordering::Equal + } else { + block_cmp + } + }); + + let mut unique_logs = Vec::new(); + for log in all_valid_logs { + if !unique_logs.contains(&log) { + unique_logs.push(log); + } + } + + print_to_terminal( + 2, + &format!( + "get_bootstrap: Consolidated {} unique logs.", + unique_logs.len(), + ), + ); + Ok((block, unique_logs)) + } + + pub fn bootstrap( + &self, + from_block: Option, + filters: Vec, + retry_params: Option<(u64, Option)>, + chain: Option, + ) -> anyhow::Result<(u64, Vec>)> { + print_to_terminal( + 2, + &format!( + "bootstrap: from_block={:?}, num_filters={}, retry_params={:?}, chain={:?}", + from_block, + filters.len(), + retry_params, + chain, + ), + ); + + let (block, consolidated_logs) = self.get_bootstrap(from_block, retry_params, chain)?; + + if consolidated_logs.is_empty() { + print_to_terminal(2,"bootstrap: No logs retrieved after consolidation. Returning empty results for filters."); + return Ok((block, filters.iter().map(|_| Vec::new()).collect())); + } + + let mut results_per_filter: Vec> = Vec::new(); + for filter in filters { + let filtered_logs = eth_apply_filter(&consolidated_logs, &filter); + results_per_filter.push(filtered_logs); + } + + print_to_terminal( + 2, + &format!( + "bootstrap: Applied {} filters to bootstrapped logs.", + results_per_filter.len(), + ), + ); + Ok((block, results_per_filter)) + } +} + +impl Serialize for ManifestItem { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut state = serializer.serialize_struct("ManifestItem", 4)?; + state.serialize_field("metadata", &self.metadata)?; + state.serialize_field("is_empty", &self.is_empty)?; + state.serialize_field("file_hash", &self.file_hash)?; + state.serialize_field("file_name", &self.file_name)?; + state.end() + } +} + +impl<'de> Deserialize<'de> for ManifestItem { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + #[derive(Deserialize)] + #[serde(field_identifier, rename_all = "snake_case")] + enum Field { + Metadata, + IsEmpty, + FileHash, + FileName, + } + + struct ManifestItemVisitor; + + impl<'de> Visitor<'de> for ManifestItemVisitor { + type Value = ManifestItem; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("struct ManifestItem") + } + + fn visit_map(self, mut map: V) -> Result + where + V: MapAccess<'de>, + { + let mut metadata = None; + let mut is_empty = None; + let mut file_hash = None; + let mut file_name = None; + + while let Some(key) = map.next_key()? { + match key { + Field::Metadata => { + if metadata.is_some() { + return Err(de::Error::duplicate_field("metadata")); + } + metadata = Some(map.next_value()?); + } + Field::IsEmpty => { + if is_empty.is_some() { + return Err(de::Error::duplicate_field("is_empty")); + } + is_empty = Some(map.next_value()?); + } + Field::FileHash => { + if file_hash.is_some() { + return Err(de::Error::duplicate_field("file_hash")); + } + file_hash = Some(map.next_value()?); + } + Field::FileName => { + if file_name.is_some() { + return Err(de::Error::duplicate_field("file_name")); + } + file_name = Some(map.next_value()?); + } + } + } + + let metadata = metadata.ok_or_else(|| de::Error::missing_field("metadata"))?; + let is_empty = is_empty.ok_or_else(|| de::Error::missing_field("is_empty"))?; + let file_hash = file_hash.ok_or_else(|| de::Error::missing_field("file_hash"))?; + let file_name = file_name.ok_or_else(|| de::Error::missing_field("file_name"))?; + + Ok(ManifestItem { + metadata, + is_empty, + file_hash, + file_name, + }) + } + } + + deserializer.deserialize_struct( + "ManifestItem", + &["metadata", "is_empty", "file_hash", "file_name"], + ManifestItemVisitor, + ) + } +} + +impl Serialize for Manifest { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut state = serializer.serialize_struct("Manifest", 4)?; + state.serialize_field("items", &self.items)?; + state.serialize_field("manifest_filename", &self.manifest_filename)?; + state.serialize_field("chain_id", &self.chain_id)?; + state.serialize_field("protocol_version", &self.protocol_version)?; + state.end() + } +} + +impl<'de> Deserialize<'de> for Manifest { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + #[derive(Deserialize)] + #[serde(field_identifier, rename_all = "snake_case")] + enum Field { + Items, + ManifestFilename, + ChainId, + ProtocolVersion, + } + + struct ManifestVisitor; + + impl<'de> Visitor<'de> for ManifestVisitor { + type Value = Manifest; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("struct Manifest") + } + + fn visit_map(self, mut map: V) -> Result + where + V: MapAccess<'de>, + { + let mut items = None; + let mut manifest_filename = None; + let mut chain_id = None; + let mut protocol_version = None; + + while let Some(key) = map.next_key()? { + match key { + Field::Items => { + if items.is_some() { + return Err(de::Error::duplicate_field("items")); + } + items = Some(map.next_value()?); + } + Field::ManifestFilename => { + if manifest_filename.is_some() { + return Err(de::Error::duplicate_field("manifest_filename")); + } + manifest_filename = Some(map.next_value()?); + } + Field::ChainId => { + if chain_id.is_some() { + return Err(de::Error::duplicate_field("chain_id")); + } + chain_id = Some(map.next_value()?); + } + Field::ProtocolVersion => { + if protocol_version.is_some() { + return Err(de::Error::duplicate_field("protocol_version")); + } + protocol_version = Some(map.next_value()?); + } + } + } + + let items = items.ok_or_else(|| de::Error::missing_field("items"))?; + let manifest_filename = manifest_filename + .ok_or_else(|| de::Error::missing_field("manifest_filename"))?; + let chain_id = chain_id.ok_or_else(|| de::Error::missing_field("chain_id"))?; + let protocol_version = + protocol_version.ok_or_else(|| de::Error::missing_field("protocol_version"))?; + + Ok(Manifest { + items, + manifest_filename, + chain_id, + protocol_version, + }) + } + } + + deserializer.deserialize_struct( + "Manifest", + &["items", "manifest_filename", "chain_id", "protocol_version"], + ManifestVisitor, + ) + } +} + +impl Serialize for GetLogsByRangeRequest { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut state = serializer.serialize_struct("GetLogsByRangeRequest", 2)?; + state.serialize_field("from_block", &self.from_block)?; + state.serialize_field("to_block", &self.to_block)?; + state.end() + } +} + +impl<'de> Deserialize<'de> for GetLogsByRangeRequest { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + #[derive(Deserialize)] + #[serde(field_identifier, rename_all = "snake_case")] + enum Field { + FromBlock, + ToBlock, + } + + struct GetLogsByRangeRequestVisitor; + + impl<'de> Visitor<'de> for GetLogsByRangeRequestVisitor { + type Value = GetLogsByRangeRequest; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("struct GetLogsByRangeRequest") + } + + fn visit_map(self, mut map: V) -> Result + where + V: MapAccess<'de>, + { + let mut from_block = None; + let mut to_block = None; + + while let Some(key) = map.next_key()? { + match key { + Field::FromBlock => { + if from_block.is_some() { + return Err(de::Error::duplicate_field("from_block")); + } + from_block = Some(map.next_value()?); + } + Field::ToBlock => { + if to_block.is_some() { + return Err(de::Error::duplicate_field("to_block")); + } + to_block = Some(map.next_value()?); + } + } + } + + let from_block = + from_block.ok_or_else(|| de::Error::missing_field("from_block"))?; + + Ok(GetLogsByRangeRequest { + from_block, + to_block, + }) + } + } + + deserializer.deserialize_struct( + "GetLogsByRangeRequest", + &["from_block", "to_block"], + GetLogsByRangeRequestVisitor, + ) + } +} + +impl Serialize for CacherStatus { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut state = serializer.serialize_struct("CacherStatus", 8)?; + state.serialize_field("last_cached_block", &self.last_cached_block)?; + state.serialize_field("chain_id", &self.chain_id)?; + state.serialize_field("protocol_version", &self.protocol_version)?; + state.serialize_field( + "next_cache_attempt_in_seconds", + &self.next_cache_attempt_in_seconds, + )?; + state.serialize_field("manifest_filename", &self.manifest_filename)?; + state.serialize_field("log_files_count", &self.log_files_count)?; + state.serialize_field("our_address", &self.our_address)?; + state.serialize_field("is_providing", &self.is_providing)?; + state.end() + } +} + +impl<'de> Deserialize<'de> for CacherStatus { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + #[derive(Deserialize)] + #[serde(field_identifier, rename_all = "snake_case")] + enum Field { + LastCachedBlock, + ChainId, + ProtocolVersion, + NextCacheAttemptInSeconds, + ManifestFilename, + LogFilesCount, + OurAddress, + IsProviding, + } + + struct CacherStatusVisitor; + + impl<'de> Visitor<'de> for CacherStatusVisitor { + type Value = CacherStatus; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("struct CacherStatus") + } + + fn visit_map(self, mut map: V) -> Result + where + V: MapAccess<'de>, + { + let mut last_cached_block = None; + let mut chain_id = None; + let mut protocol_version = None; + let mut next_cache_attempt_in_seconds = None; + let mut manifest_filename = None; + let mut log_files_count = None; + let mut our_address = None; + let mut is_providing = None; + + while let Some(key) = map.next_key()? { + match key { + Field::LastCachedBlock => { + if last_cached_block.is_some() { + return Err(de::Error::duplicate_field("last_cached_block")); + } + last_cached_block = Some(map.next_value()?); + } + Field::ChainId => { + if chain_id.is_some() { + return Err(de::Error::duplicate_field("chain_id")); + } + chain_id = Some(map.next_value()?); + } + Field::ProtocolVersion => { + if protocol_version.is_some() { + return Err(de::Error::duplicate_field("protocol_version")); + } + protocol_version = Some(map.next_value()?); + } + Field::NextCacheAttemptInSeconds => { + if next_cache_attempt_in_seconds.is_some() { + return Err(de::Error::duplicate_field( + "next_cache_attempt_in_seconds", + )); + } + next_cache_attempt_in_seconds = Some(map.next_value()?); + } + Field::ManifestFilename => { + if manifest_filename.is_some() { + return Err(de::Error::duplicate_field("manifest_filename")); + } + manifest_filename = Some(map.next_value()?); + } + Field::LogFilesCount => { + if log_files_count.is_some() { + return Err(de::Error::duplicate_field("log_files_count")); + } + log_files_count = Some(map.next_value()?); + } + Field::OurAddress => { + if our_address.is_some() { + return Err(de::Error::duplicate_field("our_address")); + } + our_address = Some(map.next_value()?); + } + Field::IsProviding => { + if is_providing.is_some() { + return Err(de::Error::duplicate_field("is_providing")); + } + is_providing = Some(map.next_value()?); + } + } + } + + let last_cached_block = last_cached_block + .ok_or_else(|| de::Error::missing_field("last_cached_block"))?; + let chain_id = chain_id.ok_or_else(|| de::Error::missing_field("chain_id"))?; + let protocol_version = + protocol_version.ok_or_else(|| de::Error::missing_field("protocol_version"))?; + let manifest_filename = manifest_filename + .ok_or_else(|| de::Error::missing_field("manifest_filename"))?; + let log_files_count = + log_files_count.ok_or_else(|| de::Error::missing_field("log_files_count"))?; + let our_address = + our_address.ok_or_else(|| de::Error::missing_field("our_address"))?; + let is_providing = + is_providing.ok_or_else(|| de::Error::missing_field("is_providing"))?; + + Ok(CacherStatus { + last_cached_block, + chain_id, + protocol_version, + next_cache_attempt_in_seconds, + manifest_filename, + log_files_count, + our_address, + is_providing, + }) + } + } + + deserializer.deserialize_struct( + "CacherStatus", + &[ + "last_cached_block", + "chain_id", + "protocol_version", + "next_cache_attempt_in_seconds", + "manifest_filename", + "log_files_count", + "our_address", + "is_providing", + ], + CacherStatusVisitor, + ) + } +} + +impl Serialize for CacherRequest { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + match self { + CacherRequest::GetManifest => { + let mut map = serializer.serialize_map(Some(1))?; + map.serialize_entry("GetManifest", &())?; + map.end() + } + CacherRequest::GetLogCacheContent(path) => { + let mut map = serializer.serialize_map(Some(1))?; + map.serialize_entry("GetLogCacheContent", path)?; + map.end() + } + CacherRequest::GetStatus => { + let mut map = serializer.serialize_map(Some(1))?; + map.serialize_entry("GetStatus", &())?; + map.end() + } + CacherRequest::GetLogsByRange(request) => { + let mut map = serializer.serialize_map(Some(1))?; + map.serialize_entry("GetLogsByRange", request)?; + map.end() + } + CacherRequest::StartProviding => { + let mut map = serializer.serialize_map(Some(1))?; + map.serialize_entry("StartProviding", &())?; + map.end() + } + CacherRequest::StopProviding => { + let mut map = serializer.serialize_map(Some(1))?; + map.serialize_entry("StopProviding", &())?; + map.end() + } + CacherRequest::SetNodes(nodes) => { + let mut map = serializer.serialize_map(Some(1))?; + map.serialize_entry("SetNodes", nodes)?; + map.end() + } + CacherRequest::Reset(nodes) => { + let mut map = serializer.serialize_map(Some(1))?; + map.serialize_entry("Reset", nodes)?; + map.end() + } + } + } +} + +impl<'de> Deserialize<'de> for CacherRequest { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct CacherRequestVisitor; + + impl<'de> Visitor<'de> for CacherRequestVisitor { + type Value = CacherRequest; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter + .write_str("a map with a single key representing the CacherRequest variant") + } + + fn visit_map(self, mut map: A) -> Result + where + A: MapAccess<'de>, + { + let (variant, value) = map + .next_entry::()? + .ok_or_else(|| de::Error::invalid_length(0, &self))?; + + match variant.as_str() { + "GetManifest" => Ok(CacherRequest::GetManifest), + "GetLogCacheContent" => { + let path = serde_json::from_value(value).map_err(de::Error::custom)?; + Ok(CacherRequest::GetLogCacheContent(path)) + } + "GetStatus" => Ok(CacherRequest::GetStatus), + "GetLogsByRange" => { + let request = serde_json::from_value(value).map_err(de::Error::custom)?; + Ok(CacherRequest::GetLogsByRange(request)) + } + "StartProviding" => Ok(CacherRequest::StartProviding), + "StopProviding" => Ok(CacherRequest::StopProviding), + "SetNodes" => { + let nodes = serde_json::from_value(value).map_err(de::Error::custom)?; + Ok(CacherRequest::SetNodes(nodes)) + } + "Reset" => { + let nodes = serde_json::from_value(value).map_err(de::Error::custom)?; + Ok(CacherRequest::Reset(nodes)) + } + _ => Err(de::Error::unknown_variant( + &variant, + &[ + "GetManifest", + "GetLogCacheContent", + "GetStatus", + "GetLogsByRange", + "StartProviding", + "StopProviding", + "SetNodes", + "Reset", + ], + )), + } + } + } + + deserializer.deserialize_map(CacherRequestVisitor) + } +} + +impl Serialize for CacherResponse { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + match self { + CacherResponse::GetManifest(manifest) => { + let mut map = serializer.serialize_map(Some(1))?; + map.serialize_entry("GetManifest", manifest)?; + map.end() + } + CacherResponse::GetLogCacheContent(result) => { + let mut map = serializer.serialize_map(Some(1))?; + map.serialize_entry("GetLogCacheContent", result)?; + map.end() + } + CacherResponse::GetStatus(status) => { + let mut map = serializer.serialize_map(Some(1))?; + map.serialize_entry("GetStatus", status)?; + map.end() + } + CacherResponse::GetLogsByRange(result) => { + let mut map = serializer.serialize_map(Some(1))?; + map.serialize_entry("GetLogsByRange", result)?; + map.end() + } + CacherResponse::StartProviding(result) => { + let mut map = serializer.serialize_map(Some(1))?; + map.serialize_entry("StartProviding", result)?; + map.end() + } + CacherResponse::StopProviding(result) => { + let mut map = serializer.serialize_map(Some(1))?; + map.serialize_entry("StopProviding", result)?; + map.end() + } + CacherResponse::Rejected => { + let mut map = serializer.serialize_map(Some(1))?; + map.serialize_entry("Rejected", &())?; + map.end() + } + CacherResponse::IsStarting => { + let mut map = serializer.serialize_map(Some(1))?; + map.serialize_entry("IsStarting", &())?; + map.end() + } + CacherResponse::SetNodes(result) => { + let mut map = serializer.serialize_map(Some(1))?; + map.serialize_entry("SetNodes", result)?; + map.end() + } + CacherResponse::Reset(result) => { + let mut map = serializer.serialize_map(Some(1))?; + map.serialize_entry("Reset", result)?; + map.end() + } + } + } +} + +impl<'de> Deserialize<'de> for CacherResponse { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct CacherResponseVisitor; + + impl<'de> Visitor<'de> for CacherResponseVisitor { + type Value = CacherResponse; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter + .write_str("a map with a single key representing the CacherResponse variant") + } + + fn visit_map(self, mut map: A) -> Result + where + A: MapAccess<'de>, + { + let (variant, value) = map + .next_entry::()? + .ok_or_else(|| de::Error::invalid_length(0, &self))?; + + match variant.as_str() { + "GetManifest" => { + let manifest = serde_json::from_value(value).map_err(de::Error::custom)?; + Ok(CacherResponse::GetManifest(manifest)) + } + "GetLogCacheContent" => { + let result = serde_json::from_value(value).map_err(de::Error::custom)?; + Ok(CacherResponse::GetLogCacheContent(result)) + } + "GetStatus" => { + let status = serde_json::from_value(value).map_err(de::Error::custom)?; + Ok(CacherResponse::GetStatus(status)) + } + "GetLogsByRange" => { + let result = serde_json::from_value(value).map_err(de::Error::custom)?; + Ok(CacherResponse::GetLogsByRange(result)) + } + "StartProviding" => { + let result = serde_json::from_value(value).map_err(de::Error::custom)?; + Ok(CacherResponse::StartProviding(result)) + } + "StopProviding" => { + let result = serde_json::from_value(value).map_err(de::Error::custom)?; + Ok(CacherResponse::StopProviding(result)) + } + "Rejected" => Ok(CacherResponse::Rejected), + "IsStarting" => Ok(CacherResponse::IsStarting), + "SetNodes" => { + let result = serde_json::from_value(value).map_err(de::Error::custom)?; + Ok(CacherResponse::SetNodes(result)) + } + "Reset" => { + let result = serde_json::from_value(value).map_err(de::Error::custom)?; + Ok(CacherResponse::Reset(result)) + } + _ => Err(de::Error::unknown_variant( + &variant, + &[ + "GetManifest", + "GetLogCacheContent", + "GetStatus", + "GetLogsByRange", + "StartProviding", + "StopProviding", + "Rejected", + "IsStarting", + "SetNodes", + "Reset", + ], + )), + } + } + } + + deserializer.deserialize_map(CacherResponseVisitor) + } +} + +impl Serialize for LogsMetadata { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut state = serializer.serialize_struct("LogsMetadata", 6)?; + state.serialize_field("chainId", &self.chain_id)?; + state.serialize_field("fromBlock", &self.from_block)?; + state.serialize_field("toBlock", &self.to_block)?; + state.serialize_field("timeCreated", &self.time_created)?; + state.serialize_field("createdBy", &self.created_by)?; + state.serialize_field("signature", &self.signature)?; + state.end() + } +} + +impl<'de> Deserialize<'de> for LogsMetadata { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + #[derive(Deserialize)] + #[serde(field_identifier, rename_all = "camelCase")] + enum Field { + ChainId, + FromBlock, + ToBlock, + TimeCreated, + CreatedBy, + Signature, + } + + struct LogsMetadataVisitor; + + impl<'de> Visitor<'de> for LogsMetadataVisitor { + type Value = LogsMetadata; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("struct LogsMetadata") + } + + fn visit_map(self, mut map: V) -> Result + where + V: MapAccess<'de>, + { + let mut chain_id = None; + let mut from_block = None; + let mut to_block = None; + let mut time_created = None; + let mut created_by = None; + let mut signature = None; + + while let Some(key) = map.next_key()? { + match key { + Field::ChainId => { + if chain_id.is_some() { + return Err(de::Error::duplicate_field("chainId")); + } + chain_id = Some(map.next_value()?); + } + Field::FromBlock => { + if from_block.is_some() { + return Err(de::Error::duplicate_field("fromBlock")); + } + from_block = Some(map.next_value()?); + } + Field::ToBlock => { + if to_block.is_some() { + return Err(de::Error::duplicate_field("toBlock")); + } + to_block = Some(map.next_value()?); + } + Field::TimeCreated => { + if time_created.is_some() { + return Err(de::Error::duplicate_field("timeCreated")); + } + time_created = Some(map.next_value()?); + } + Field::CreatedBy => { + if created_by.is_some() { + return Err(de::Error::duplicate_field("createdBy")); + } + created_by = Some(map.next_value()?); + } + Field::Signature => { + if signature.is_some() { + return Err(de::Error::duplicate_field("signature")); + } + signature = Some(map.next_value()?); + } + } + } + + let chain_id = chain_id.ok_or_else(|| de::Error::missing_field("chainId"))?; + let from_block = from_block.ok_or_else(|| de::Error::missing_field("fromBlock"))?; + let to_block = to_block.ok_or_else(|| de::Error::missing_field("toBlock"))?; + let time_created = + time_created.ok_or_else(|| de::Error::missing_field("timeCreated"))?; + let created_by = created_by.ok_or_else(|| de::Error::missing_field("createdBy"))?; + let signature = signature.ok_or_else(|| de::Error::missing_field("signature"))?; + + Ok(LogsMetadata { + chain_id, + from_block, + to_block, + time_created, + created_by, + signature, + }) + } + } + + deserializer.deserialize_struct( + "LogsMetadata", + &[ + "chainId", + "fromBlock", + "toBlock", + "timeCreated", + "createdBy", + "signature", + ], + LogsMetadataVisitor, + ) + } +} + +impl Serialize for GetLogsByRangeOkResponse { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + match self { + GetLogsByRangeOkResponse::Logs(tuple) => { + let mut map = serializer.serialize_map(Some(1))?; + map.serialize_entry("Logs", tuple)?; + map.end() + } + GetLogsByRangeOkResponse::Latest(block) => { + let mut map = serializer.serialize_map(Some(1))?; + map.serialize_entry("Latest", block)?; + map.end() + } + } + } +} + +impl<'de> Deserialize<'de> for GetLogsByRangeOkResponse { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct GetLogsByRangeOkResponseVisitor; + + impl<'de> Visitor<'de> for GetLogsByRangeOkResponseVisitor { + type Value = GetLogsByRangeOkResponse; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str( + "a map with a single key representing the GetLogsByRangeOkResponse variant", + ) + } + + fn visit_map(self, mut map: A) -> Result + where + A: MapAccess<'de>, + { + let (variant, value) = map + .next_entry::()? + .ok_or_else(|| de::Error::invalid_length(0, &self))?; + + match variant.as_str() { + "Logs" => { + let tuple = serde_json::from_value(value).map_err(de::Error::custom)?; + Ok(GetLogsByRangeOkResponse::Logs(tuple)) + } + "Latest" => { + let block = serde_json::from_value(value).map_err(de::Error::custom)?; + Ok(GetLogsByRangeOkResponse::Latest(block)) + } + _ => Err(de::Error::unknown_variant(&variant, &["Logs", "Latest"])), + } + } + } + + deserializer.deserialize_map(GetLogsByRangeOkResponseVisitor) + } +} diff --git a/src/kernel_types.rs b/src/kernel_types.rs index 43bc01e..5022cb7 100644 --- a/src/kernel_types.rs +++ b/src/kernel_types.rs @@ -1,4 +1,4 @@ -use crate::kinode::process::standard as wit; +use crate::hyperware::process::standard as wit; use crate::{Address, ProcessId}; use serde::{Deserialize, Serialize}; use std::collections::{HashMap, HashSet}; @@ -7,7 +7,7 @@ use std::hash::{Hash, Hasher}; // // process-facing kernel types, used for process // management and message-passing -// matches types in kinode.wit +// matches types in hyperware.wit // pub type Context = Vec; @@ -265,7 +265,7 @@ impl StateError { // package types // -/// Represents the metadata associated with a kinode package, which is an ERC721 compatible token. +/// Represents the metadata associated with a hyperware package, which is an ERC721 compatible token. /// This is deserialized from the `metadata.json` file in a package. /// Fields: /// - `name`: An optional field representing the display name of the package. This does not have to be unique, and is not used for identification purposes. @@ -284,12 +284,12 @@ pub struct Erc721Metadata { pub properties: Erc721Properties, } -/// Represents critical fields of a kinode package in an ERC721 compatible format. +/// Represents critical fields of a hyperware package in an ERC721 compatible format. /// This follows the [ERC1155](https://github.com/ethereum/ercs/blob/master/ERCS/erc-1155.md#erc-1155-metadata-uri-json-schema) metadata standard. /// /// Fields: /// - `package_name`: The unique name of the package, used in the [`crate::PackageId`], e.g. `package_name:publisher`. -/// - `publisher`: The KNS identity of the package publisher used in the [`crate::PackageId`], e.g. `package_name:publisher` +/// - `publisher`: The HNS identity of the package publisher used in the [`crate::PackageId`], e.g. `package_name:publisher` /// - `current_version`: A string representing the current version of the package, e.g. `1.0.0`. /// - `mirrors`: A list of NodeIds where the package can be found, providing redundancy. /// - `code_hashes`: A map from version names to their respective SHA-256 hashes. diff --git a/src/kimap.rs b/src/kimap.rs deleted file mode 100644 index 197472f..0000000 --- a/src/kimap.rs +++ /dev/null @@ -1,616 +0,0 @@ -use crate::eth::{EthError, Provider}; -use crate::kimap::contract::getCall; -use crate::net; -use alloy::rpc::types::request::{TransactionInput, TransactionRequest}; -use alloy::{hex, primitives::keccak256}; -use alloy_primitives::{Address, Bytes, FixedBytes, B256}; -use alloy_sol_types::{SolCall, SolEvent, SolValue}; -use contract::tokenCall; -use serde::{Deserialize, Serialize}; -use std::error::Error; -use std::fmt; -use std::str::FromStr; - -/// kimap deployment address on base -pub const KIMAP_ADDRESS: &'static str = "0x000000000033e5CCbC52Ec7BDa87dB768f9aA93F"; -/// base chain id -pub const KIMAP_CHAIN_ID: u64 = 8453; -/// first block (minus one) of kimap deployment on base -pub const KIMAP_FIRST_BLOCK: u64 = 25_346_377; -/// the root hash of kimap, empty bytes32 -pub const KIMAP_ROOT_HASH: &'static str = - "0x0000000000000000000000000000000000000000000000000000000000000000"; - -/// Sol structures for Kimap requests -pub mod contract { - use alloy_sol_macro::sol; - - sol! { - /// Emitted when a new namespace entry is minted. - /// - parenthash: The hash of the parent namespace entry. - /// - childhash: The hash of the minted namespace entry's full path. - /// - labelhash: The hash of only the label (the final entry in the path). - /// - label: The label (the final entry in the path) of the new entry. - event Mint( - bytes32 indexed parenthash, - bytes32 indexed childhash, - bytes indexed labelhash, - bytes label - ); - - /// Emitted when a fact is created on an existing namespace entry. - /// Facts are immutable and may only be written once. A fact label is - /// prepended with an exclamation mark (!) to indicate that it is a fact. - /// - parenthash The hash of the parent namespace entry. - /// - facthash The hash of the newly created fact's full path. - /// - labelhash The hash of only the label (the final entry in the path). - /// - label The label of the fact. - /// - data The data stored at the fact. - event Fact( - bytes32 indexed parenthash, - bytes32 indexed facthash, - bytes indexed labelhash, - bytes label, - bytes data - ); - - /// Emitted when a new note is created on an existing namespace entry. - /// Notes are mutable. A note label is prepended with a tilde (~) to indicate - /// that it is a note. - /// - parenthash: The hash of the parent namespace entry. - /// - notehash: The hash of the newly created note's full path. - /// - labelhash: The hash of only the label (the final entry in the path). - /// - label: The label of the note. - /// - data: The data stored at the note. - event Note( - bytes32 indexed parenthash, - bytes32 indexed notehash, - bytes indexed labelhash, - bytes label, - bytes data - ); - - /// Emitted when a gene is set for an existing namespace entry. - /// A gene is a specific TBA implementation which will be applied to all - /// sub-entries of the namespace entry. - /// - entry: The namespace entry's namehash. - /// - gene: The address of the TBA implementation. - event Gene(bytes32 indexed entry, address indexed gene); - - /// Emitted when the zeroth namespace entry is minted. - /// Occurs exactly once at initialization. - /// - zeroTba: The address of the zeroth TBA - event Zero(address indexed zeroTba); - - /// Emitted when a namespace entry is transferred from one address - /// to another. - /// - from: The address of the sender. - /// - to: The address of the recipient. - /// - id: The namehash of the namespace entry (converted to uint256). - event Transfer( - address indexed from, - address indexed to, - uint256 indexed id - ); - - /// Emitted when a namespace entry is approved for transfer. - /// - owner: The address of the owner. - /// - spender: The address of the spender. - /// - id: The namehash of the namespace entry (converted to uint256). - event Approval( - address indexed owner, - address indexed spender, - uint256 indexed id - ); - - /// Emitted when an operator is approved for all of an owner's - /// namespace entries. - /// - owner: The address of the owner. - /// - operator: The address of the operator. - /// - approved: Whether the operator is approved. - event ApprovalForAll( - address indexed owner, - address indexed operator, - bool approved - ); - - /// Retrieves information about a specific namespace entry. - /// - namehash The namehash of the namespace entry to query. - /// - /// Returns: - /// - tba: The address of the token-bound account associated - /// with the entry. - /// - owner: The address of the entry owner. - /// - data: The note or fact bytes associated with the entry - /// (empty if not a note or fact). - function get( - bytes32 namehash - ) external view returns (address tba, address owner, bytes memory data); - - /// Mints a new namespace entry and creates a token-bound account for - /// it. Must be called by a parent namespace entry token-bound account. - /// - who: The address to own the new namespace entry. - /// - label: The label to mint beneath the calling parent entry. - /// - initialization: Initialization calldata applied to the new - /// minted entry's token-bound account. - /// - erc721Data: ERC-721 data -- passed to comply with - /// `ERC721TokenReceiver.onERC721Received()`. - /// - implementation: The address of the implementation contract for - /// the token-bound account: this will be overriden by the gene if the - /// parent entry has one set. - /// - /// Returns: - /// - tba: The address of the new entry's token-bound account. - function mint( - address who, - bytes calldata label, - bytes calldata initialization, - bytes calldata erc721Data, - address implementation - ) external returns (address tba); - - /// Sets the gene for the calling namespace entry. - /// - _gene: The address of the TBA implementation to set for all - /// children of the calling namespace entry. - function gene(address _gene) external; - - /// Creates a new fact beneath the calling namespace entry. - /// - fact: The fact label to create. Must be prepended with an - /// exclamation mark (!). - /// - data: The data to be stored at the fact. - /// - /// Returns: - /// - facthash: The namehash of the newly created fact. - function fact( - bytes calldata fact, - bytes calldata data - ) external returns (bytes32 facthash); - - /// Creates a new note beneath the calling namespace entry. - /// - note: The note label to create. Must be prepended with a tilde (~). - /// - data: The data to be stored at the note. - /// - /// Returns: - /// - notehash: The namehash of the newly created note. - function note( - bytes calldata note, - bytes calldata data - ) external returns (bytes32 notehash); - - /// Retrieves the token-bound account address of a namespace entry. - /// - entry: The entry namehash (as uint256) for which to get the - /// token-bound account. - /// - /// Returns: - /// - tba: The token-bound account address of the namespace entry. - function tbaOf(uint256 entry) external view returns (address tba); - - function balanceOf(address owner) external view returns (uint256); - - function getApproved(uint256 entry) external view returns (address); - - function isApprovedForAll( - address owner, - address operator - ) external view returns (bool); - - function ownerOf(uint256 entry) external view returns (address); - - function setApprovalForAll(address operator, bool approved) external; - - function approve(address spender, uint256 entry) external; - - function safeTransferFrom(address from, address to, uint256 id) external; - - function safeTransferFrom( - address from, - address to, - uint256 id, - bytes calldata data - ) external; - - function transferFrom(address from, address to, uint256 id) external; - - function supportsInterface(bytes4 interfaceId) external view returns (bool); - - /// Gets the token identifier that owns this token-bound account (TBA). - /// This is a core function of the ERC-6551 standard that returns the - /// identifying information about the NFT that owns this account. - /// The return values are constant and cannot change over time. - /// - /// Returns: - /// - chainId: The EIP-155 chain ID where the owning NFT exists - /// - tokenContract: The contract address of the owning NFT - /// - tokenId: The token ID of the owning NFT - function token() - external - view - returns (uint256 chainId, address tokenContract, uint256 tokenId); - } -} - -/// A mint log from the kimap, converted to a 'resolved' format using -/// namespace data saved in the kns-indexer. -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct Mint { - pub name: String, - pub parent_path: String, -} - -/// A note log from the kimap, converted to a 'resolved' format using -/// namespace data saved in the kns-indexer -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct Note { - pub note: String, - pub parent_path: String, - pub data: Bytes, -} - -/// A fact log from the kimap, converted to a 'resolved' format using -/// namespace data saved in the kns-indexer -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct Fact { - pub fact: String, - pub parent_path: String, - pub data: Bytes, -} - -/// Errors that can occur when decoding a log from the kimap using -/// [`decode_mint_log()`] or [`decode_note_log()`]. -#[derive(Clone, Debug, Deserialize, Serialize)] -pub enum DecodeLogError { - /// The log's topic is not a mint or note event. - UnexpectedTopic(B256), - /// The name is not valid (according to [`valid_name`]). - InvalidName(String), - /// An error occurred while decoding the log. - DecodeError(String), - /// The parent name could not be resolved with `kns-indexer`. - UnresolvedParent(String), -} - -impl fmt::Display for DecodeLogError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - DecodeLogError::UnexpectedTopic(topic) => write!(f, "Unexpected topic: {:?}", topic), - DecodeLogError::InvalidName(name) => write!(f, "Invalid name: {}", name), - DecodeLogError::DecodeError(err) => write!(f, "Decode error: {}", err), - DecodeLogError::UnresolvedParent(parent) => { - write!(f, "Could not resolve parent: {}", parent) - } - } - } -} - -impl Error for DecodeLogError {} - -/// Canonical function to determine if a kimap entry is valid. This should -/// be used whenever reading a new kimap entry from a mints query, because -/// while most frontends will enforce these rules, it is possible to post -/// invalid names to the kimap contract. -/// -/// This checks a **single name**, not the full path-name. A full path-name -/// is comprised of valid names separated by `.` -pub fn valid_entry(entry: &str, note: bool, fact: bool) -> bool { - if note && fact { - return false; - } - if note { - valid_note(entry) - } else if fact { - valid_fact(entry) - } else { - valid_name(entry) - } -} - -pub fn valid_name(name: &str) -> bool { - name.is_ascii() - && name.len() >= 1 - && name - .chars() - .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-') -} - -pub fn valid_note(note: &str) -> bool { - note.is_ascii() - && note.len() >= 2 - && note.chars().next() == Some('~') - && note - .chars() - .skip(1) - .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-') -} - -pub fn valid_fact(fact: &str) -> bool { - fact.is_ascii() - && fact.len() >= 2 - && fact.chars().next() == Some('!') - && fact - .chars() - .skip(1) - .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-') -} - -/// Produce a namehash from a kimap name. -pub fn namehash(name: &str) -> String { - let mut node = B256::default(); - - let mut labels: Vec<&str> = name.split('.').collect(); - labels.reverse(); - - for label in labels.iter() { - let l = keccak256(label); - node = keccak256((node, l).abi_encode_packed()); - } - format!("0x{}", hex::encode(node)) -} - -/// Decode a mint log from the kimap into a 'resolved' format. -/// -/// Uses [`valid_name()`] to check if the name is valid. -pub fn decode_mint_log(log: &crate::eth::Log) -> Result { - let contract::Note::SIGNATURE_HASH = log.topics()[0] else { - return Err(DecodeLogError::UnexpectedTopic(log.topics()[0])); - }; - let decoded = contract::Mint::decode_log_data(log.data(), true) - .map_err(|e| DecodeLogError::DecodeError(e.to_string()))?; - let name = String::from_utf8_lossy(&decoded.label).to_string(); - if !valid_name(&name) { - return Err(DecodeLogError::InvalidName(name)); - } - match resolve_parent(log, None) { - Some(parent_path) => Ok(Mint { name, parent_path }), - None => Err(DecodeLogError::UnresolvedParent(name)), - } -} - -/// Decode a note log from the kimap into a 'resolved' format. -/// -/// Uses [`valid_name()`] to check if the name is valid. -pub fn decode_note_log(log: &crate::eth::Log) -> Result { - let contract::Note::SIGNATURE_HASH = log.topics()[0] else { - return Err(DecodeLogError::UnexpectedTopic(log.topics()[0])); - }; - let decoded = contract::Note::decode_log_data(log.data(), true) - .map_err(|e| DecodeLogError::DecodeError(e.to_string()))?; - let note = String::from_utf8_lossy(&decoded.label).to_string(); - if !valid_note(¬e) { - return Err(DecodeLogError::InvalidName(note)); - } - match resolve_parent(log, None) { - Some(parent_path) => Ok(Note { - note, - parent_path, - data: decoded.data, - }), - None => Err(DecodeLogError::UnresolvedParent(note)), - } -} - -pub fn decode_fact_log(log: &crate::eth::Log) -> Result { - let contract::Fact::SIGNATURE_HASH = log.topics()[0] else { - return Err(DecodeLogError::UnexpectedTopic(log.topics()[0])); - }; - let decoded = contract::Fact::decode_log_data(log.data(), true) - .map_err(|e| DecodeLogError::DecodeError(e.to_string()))?; - let fact = String::from_utf8_lossy(&decoded.label).to_string(); - if !valid_fact(&fact) { - return Err(DecodeLogError::InvalidName(fact)); - } - match resolve_parent(log, None) { - Some(parent_path) => Ok(Fact { - fact, - parent_path, - data: decoded.data, - }), - None => Err(DecodeLogError::UnresolvedParent(fact)), - } -} - -/// Given a [`crate::eth::Log`] (which must be a log from kimap), resolve the parent name -/// of the new entry or note. -pub fn resolve_parent(log: &crate::eth::Log, timeout: Option) -> Option { - let parent_hash = log.topics()[1].to_string(); - net::get_name(&parent_hash, log.block_number, timeout) -} - -/// Given a [`crate::eth::Log`] (which must be a log from kimap), resolve the full name -/// of the new entry or note. -/// -/// Uses [`valid_name()`] to check if the name is valid. -pub fn resolve_full_name(log: &crate::eth::Log, timeout: Option) -> Option { - let parent_hash = log.topics()[1].to_string(); - let parent_name = net::get_name(&parent_hash, log.block_number, timeout)?; - let log_name = match log.topics()[0] { - contract::Mint::SIGNATURE_HASH => { - let decoded = contract::Mint::decode_log_data(log.data(), true).unwrap(); - decoded.label - } - contract::Note::SIGNATURE_HASH => { - let decoded = contract::Note::decode_log_data(log.data(), true).unwrap(); - decoded.label - } - contract::Fact::SIGNATURE_HASH => { - let decoded = contract::Fact::decode_log_data(log.data(), true).unwrap(); - decoded.label - } - _ => return None, - }; - let name = String::from_utf8_lossy(&log_name); - if !valid_entry( - &name, - log.topics()[0] == contract::Note::SIGNATURE_HASH, - log.topics()[0] == contract::Fact::SIGNATURE_HASH, - ) { - return None; - } - Some(format!("{name}.{parent_name}")) -} - -/// Helper struct for reading from the kimap. -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct Kimap { - pub provider: Provider, - address: Address, -} - -impl Kimap { - /// Creates a new Kimap instance with a specified address. - /// - /// # Arguments - /// * `provider` - A reference to the Provider. - /// * `address` - The address of the Kimap contract. - pub fn new(provider: Provider, address: Address) -> Self { - Self { provider, address } - } - - /// Creates a new Kimap instance with the default address and chain ID. - pub fn default(timeout: u64) -> Self { - let provider = Provider::new(KIMAP_CHAIN_ID, timeout); - Self::new(provider, Address::from_str(KIMAP_ADDRESS).unwrap()) - } - - /// Returns the in-use Kimap contract address. - pub fn address(&self) -> &Address { - &self.address - } - - /// Gets an entry from the Kimap by its string-formatted name. - /// - /// # Parameters - /// - `path`: The name-path to get from the Kimap. - /// # Returns - /// A `Result<(Address, Address, Option), EthError>` representing the TBA, owner, - /// and value if the entry exists and is a note. - pub fn get(&self, path: &str) -> Result<(Address, Address, Option), EthError> { - let get_call = getCall { - namehash: FixedBytes::<32>::from_str(&namehash(path)) - .map_err(|_| EthError::InvalidParams)?, - } - .abi_encode(); - - let tx_req = TransactionRequest::default() - .input(TransactionInput::new(get_call.into())) - .to(self.address); - - let res_bytes = self.provider.call(tx_req, None)?; - - let res = getCall::abi_decode_returns(&res_bytes, false) - .map_err(|_| EthError::RpcMalformedResponse)?; - - let note_data = if res.data == Bytes::default() { - None - } else { - Some(res.data) - }; - - Ok((res.tba, res.owner, note_data)) - } - - /// Gets an entry from the Kimap by its hash. - /// - /// # Parameters - /// - `entryhash`: The entry to get from the Kimap. - /// # Returns - /// A `Result<(Address, Address, Option), EthError>` representing the TBA, owner, - /// and value if the entry exists and is a note. - pub fn get_hash(&self, entryhash: &str) -> Result<(Address, Address, Option), EthError> { - let get_call = getCall { - namehash: FixedBytes::<32>::from_str(entryhash).map_err(|_| EthError::InvalidParams)?, - } - .abi_encode(); - - let tx_req = TransactionRequest::default() - .input(TransactionInput::new(get_call.into())) - .to(self.address); - - let res_bytes = self.provider.call(tx_req, None)?; - - let res = getCall::abi_decode_returns(&res_bytes, false) - .map_err(|_| EthError::RpcMalformedResponse)?; - - let note_data = if res.data == Bytes::default() { - None - } else { - Some(res.data) - }; - - Ok((res.tba, res.owner, note_data)) - } - - /// Gets a namehash from an existing TBA address. - /// - /// # Parameters - /// - `tba`: The TBA to get the namehash of. - /// # Returns - /// A `Result` representing the namehash of the TBA. - pub fn get_namehash_from_tba(&self, tba: Address) -> Result { - let token_call = tokenCall {}.abi_encode(); - - let tx_req = TransactionRequest::default() - .input(TransactionInput::new(token_call.into())) - .to(tba); - - let res_bytes = self.provider.call(tx_req, None)?; - - let res = tokenCall::abi_decode_returns(&res_bytes, false) - .map_err(|_| EthError::RpcMalformedResponse)?; - - let namehash: FixedBytes<32> = res.tokenId.into(); - Ok(format!("0x{}", hex::encode(namehash))) - } - - /// Create a filter for all mint events. - pub fn mint_filter(&self) -> crate::eth::Filter { - crate::eth::Filter::new() - .address(self.address) - .event(contract::Mint::SIGNATURE) - } - - /// Create a filter for all note events. - pub fn note_filter(&self) -> crate::eth::Filter { - crate::eth::Filter::new() - .address(self.address) - .event(contract::Note::SIGNATURE) - } - - /// Create a filter for all fact events. - pub fn fact_filter(&self) -> crate::eth::Filter { - crate::eth::Filter::new() - .address(self.address) - .event(contract::Fact::SIGNATURE) - } - - /// Create a filter for a given set of specific notes. This function will - /// hash the note labels and use them as the topic3 filter. - /// - /// Example: - /// ```rust - /// let filter = kimap.notes_filter(&["~note1", "~note2"]); - /// ``` - pub fn notes_filter(&self, notes: &[&str]) -> crate::eth::Filter { - self.note_filter().topic3( - notes - .into_iter() - .map(|note| keccak256(note)) - .collect::>(), - ) - } - - /// Create a filter for a given set of specific facts. This function will - /// hash the fact labels and use them as the topic3 filter. - /// - /// Example: - /// ```rust - /// let filter = kimap.facts_filter(&["!fact1", "!fact2"]); - /// ``` - pub fn facts_filter(&self, facts: &[&str]) -> crate::eth::Filter { - self.fact_filter().topic3( - facts - .into_iter() - .map(|fact| keccak256(fact)) - .collect::>(), - ) - } -} diff --git a/src/lib.rs b/src/lib.rs index d64c769..c867924 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,7 @@ -//! Kinode process standard library for Rust compiled to Wasm -//! Must be used in context of bindings generated by `kinode.wit`. +//! Hyperware process standard library for Rust compiled to Wasm +//! Must be used in context of bindings generated by `hyperware.wit`. //! -//! This library provides a set of functions for interacting with the kinode +//! This library provides a set of functions for interacting with the hyperware //! kernel interface, which is a WIT file. The types generated by this file //! are available in processes via the wit_bindgen macro, if a process needs //! to use them directly. However, the most convenient way to do most things @@ -13,13 +13,13 @@ //! for applications that want to maximize composability and introspectability. //! For blobs, we recommend bincode to serialize and deserialize to bytes. //! -pub use crate::kinode::process::standard::*; +pub use crate::hyperware::process::standard::*; use serde_json::Value; wit_bindgen::generate!({ - path: "kinode-wit", + path: "hyperware-wit", + world: "process-lib", generate_unused_types: true, - world: "lib", }); /// Interact with the eth provider module. @@ -35,12 +35,12 @@ pub mod homepage; /// Your process must have the [`Capability`] to message and receive messages from /// `http-server:distro:sys` and/or `http-client:distro:sys` to use this module. pub mod http; +/// Interact with hypermap, the onchain namespace +pub mod hypermap; /// The types that the kernel itself uses -- warning -- these will /// be incompatible with WIT types in some cases, leading to annoying errors. /// Use only to interact with the kernel or runtime in certain ways. pub mod kernel_types; -/// Interact with kimap, the onchain namespace -pub mod kimap; /// Interact with the key_value module /// /// Your process must have the [`Capability`] to message and receive messages from @@ -54,6 +54,7 @@ pub mod logging; /// Your process must have the [`Capability`] to message and receive messages from /// `net:distro:sys` to use this module. pub mod net; +pub mod sign; /// Interact with the sqlite module /// /// Your process must have the [`Capability] to message and receive messages from @@ -77,7 +78,7 @@ pub use types::{ address::{Address, AddressParseError}, capability::Capability, lazy_load_blob::LazyLoadBlob, - message::{Message, _wit_message_to_message}, + message::{BuildError, Message, _wit_message_to_message}, on_exit::OnExit, package_id::PackageId, process_id::{ProcessId, ProcessIdParseError}, @@ -155,7 +156,7 @@ macro_rules! process_println { /// /// Example: /// ```no_run -/// use kinode_process_lib::{await_message, println}; +/// use hyperware_process_lib::{await_message, println}; /// /// loop { /// match await_message() { @@ -185,7 +186,7 @@ pub fn await_next_message_body() -> Result, SendError> { } /// Spawn a new process. This function is a wrapper around the standard `spawn()` function -/// provided in `kinode::process::standard` (which is generated by the WIT file). +/// provided in `hyperware::process::standard` (which is generated by the WIT file). pub fn spawn( name: Option<&str>, wasm_path: &str, @@ -194,7 +195,7 @@ pub fn spawn( grant_capabilities: Vec<(ProcessId, Json)>, public: bool, ) -> Result { - crate::kinode::process::standard::spawn( + crate::hyperware::process::standard::spawn( name, wasm_path, &on_exit._to_standard().map_err(|_e| SpawnError::NameTaken)?, @@ -209,7 +210,7 @@ pub fn spawn( /// /// Example usage: /// ```no_run -/// use kinode_process_lib::make_blob; +/// use hyperware_process_lib::make_blob; /// use bincode; /// use serde::{Serialize, Deserialize}; /// @@ -244,7 +245,7 @@ where /// Example: /// ```no_run /// use std::collections::{HashMap, HashSet}; -/// use kinode_process_lib::get_typed_blob; +/// use hyperware_process_lib::get_typed_blob; /// use bincode; /// use serde::{Serialize, Deserialize}; /// @@ -280,7 +281,7 @@ where /// Example: /// ```no_run /// use std::collections::{HashMap, HashSet}; -/// use kinode_process_lib::get_typed_state; +/// use hyperware_process_lib::get_typed_state; /// use bincode; /// use serde::{Serialize, Deserialize}; /// @@ -312,20 +313,31 @@ where /// See if we have the [`Capability`] to message a certain process. /// Note if you have not saved the [`Capability`], you will not be able to message the other process. pub fn can_message(address: &Address) -> bool { + let address = eval_our(address); crate::our_capabilities() .iter() - .any(|cap| cap.params == "\"messaging\"" && cap.issuer == *address) + .any(|cap| cap.params == "\"messaging\"" && cap.issuer == address) } /// Get a [`Capability`] in our store pub fn get_capability(issuer: &Address, params: &str) -> Option { + let issuer = eval_our(issuer); let params = serde_json::from_str::(params).unwrap_or_default(); crate::our_capabilities().into_iter().find(|cap| { let cap_params = serde_json::from_str::(&cap.params).unwrap_or_default(); - cap.issuer == *issuer && params == cap_params + cap.issuer == issuer && params == cap_params }) } +pub fn eval_our(address: &Address) -> Address { + let mut address = address.clone(); + if address.node() == "our" { + let our = crate::our(); + address.node = our.node().to_string() + } + address +} + /// The `Spawn!()` macro is defined here as a no-op. /// However, in practice, `kit build` will rewrite it during pre-processing. /// diff --git a/src/logging.rs b/src/logging.rs index 49cc2bb..5dadc52 100644 --- a/src/logging.rs +++ b/src/logging.rs @@ -42,7 +42,14 @@ pub struct TerminalWriterMaker { impl std::io::Write for RemoteWriter { fn write(&mut self, buf: &[u8]) -> std::io::Result { - let body = serde_json::json!({"Log": buf}); + let log = if let Ok(json_log) = serde_json::from_slice::(buf) { + serde_json::to_string(&json_log).unwrap() + } else { + let string = String::from_utf8(buf.to_vec()) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?; + string + }; + let body = serde_json::json!({"Log": log}); let body = serde_json::to_vec(&body).unwrap(); Request::to(&self.target).body(body).send().unwrap(); Ok(buf.len()) diff --git a/src/net.rs b/src/net.rs index 2403b93..371e465 100644 --- a/src/net.rs +++ b/src/net.rs @@ -7,7 +7,7 @@ use std::collections::BTreeMap; // /// The data structure used by `net:distro:sys` and the rest of the runtime to -/// represent node identities in the KNS (Kinode Name System). +/// represent node identities in the HNS (Hyperware Name System). #[derive(Clone, Debug, Serialize, Deserialize)] pub struct Identity { pub name: NodeId, @@ -15,7 +15,7 @@ pub struct Identity { pub routing: NodeRouting, } -/// Routing information for a node identity. Produced from kimap data entries +/// Routing information for a node identity. Produced from hypermap data entries /// and used to create net connections between nodes. #[derive(Clone, Debug, Serialize, Deserialize)] pub enum NodeRouting { @@ -66,9 +66,9 @@ pub enum NetAction { /// This cannot be sent locally. ConnectionRequest(NodeId), /// Can only receive from trusted source: requires net root [`crate::Capability`]. - KnsUpdate(KnsUpdate), + HnsUpdate(HnsUpdate), /// Can only receive from trusted source: requires net root [`crate::Capability`]. - KnsBatchUpdate(Vec), + HnsBatchUpdate(Vec), /// Get a list of peers with whom we have an open connection. GetPeers, /// Get the [`Identity`] struct for a single peer. @@ -108,18 +108,18 @@ pub enum NetResponse { Verified(bool), } -/// Request performed to `kns-indexer:kns-indexer:sys`, a userspace process +/// Request performed to `hns-indexer:hns-indexer:sys`, a userspace process /// installed by default. /// /// Other requests exist but are only used internally. #[derive(Debug, Serialize, Deserialize)] pub enum IndexerRequests { /// Get the name associated with a namehash. This is used to resolve namehashes - /// from events in the `kimap` contract. + /// from events in the `hypermap` contract. NamehashToName(NamehashToNameRequest), } -/// Request to resolve a namehash to a name. Hash is a namehash from `kimap`. +/// Request to resolve a namehash to a name. Hash is a namehash from `hypermap`. /// Block is optional, and if provided will return the name at that block number. /// If not provided, the latest knowledge will be returned. /// @@ -131,17 +131,17 @@ pub struct NamehashToNameRequest { pub block: u64, } -/// Response from `kns-indexer:kns-indexer:sys`. +/// Response from `hns-indexer:hns-indexer:sys`. #[derive(Debug, Serialize, Deserialize)] pub enum IndexerResponses { /// Response to [`IndexerRequests::NamehashToName`]. Name(Option), } -/// Update type used to convert kimap entries into node identities. +/// Update type used to convert hypermap entries into node identities. /// Only currently used in userspace for `eth:distro:sys` configuration. #[derive(Clone, Debug, Serialize, Deserialize, Hash, Eq, PartialEq)] -pub struct KnsUpdate { +pub struct HnsUpdate { pub name: String, pub public_key: String, pub ips: Vec, @@ -149,7 +149,7 @@ pub struct KnsUpdate { pub routers: Vec, } -impl KnsUpdate { +impl HnsUpdate { pub fn get_protocol_port(&self, protocol: &str) -> u16 { self.ports.get(protocol).cloned().unwrap_or(0) } @@ -213,7 +213,7 @@ where }) } -/// Get a [`crate::kimap::Kimap`] entry name from its namehash. +/// Get a [`crate::hypermap::Hypermap`] entry name from its namehash. /// /// Default timeout is 30 seconds. Note that the responsiveness of the indexer /// will depend on the block option used. The indexer will wait until it has @@ -222,7 +222,7 @@ pub fn get_name(namehash: T, block: Option, timeout: Option) -> Opt where T: Into, { - let res = Request::to(("our", "kns-indexer", "kns-indexer", "sys")) + let res = Request::to(("our", "hns-indexer", "hns-indexer", "sys")) .body( serde_json::to_vec(&IndexerRequests::NamehashToName(NamehashToNameRequest { hash: namehash.into(), diff --git a/src/scripting/mod.rs b/src/scripting/mod.rs index e92922a..d7575ad 100644 --- a/src/scripting/mod.rs +++ b/src/scripting/mod.rs @@ -13,7 +13,7 @@ macro_rules! script { struct Component; impl Guest for Component { fn init(our: String) { - use kinode_process_lib::{await_message, println, Address, Message, Response}; + use hyperware_process_lib::{await_message, println, Address, Message, Response}; let our: Address = our.parse().unwrap(); let Message::Request { body, @@ -54,13 +54,13 @@ macro_rules! script { /// world: "process-v0", /// }); /// -/// kinode_process_lib::widget!("My widget", create_widget); +/// hyperware_process_lib::widget!("My widget", create_widget); /// /// fn create_widget() -> String { /// return r#" /// /// -/// +/// /// /// ///

Hello World!

@@ -73,7 +73,7 @@ macro_rules! widget { struct Component; impl Guest for Component { fn init(_our: String) { - use kinode_process_lib::Request; + use hyperware_process_lib::Request; Request::to(("our", "homepage", "homepage", "sys")) .body( serde_json::json!({ diff --git a/src/sign.rs b/src/sign.rs new file mode 100644 index 0000000..3b559ec --- /dev/null +++ b/src/sign.rs @@ -0,0 +1,38 @@ +use crate::{last_blob, Address, Request}; +// TODO: use WIT types + +pub fn net_key_sign(message: Vec) -> anyhow::Result> { + Request::to(("our", "sign", "sign", "sys")) + .body("\"NetKeySign\"") + .blob_bytes(message) + .send_and_await_response(10)??; + Ok(last_blob().unwrap().bytes) +} + +pub fn net_key_verify( + message: Vec, + signer: &Address, + signature: Vec, +) -> anyhow::Result { + let response = Request::to(("our", "sign", "sign", "sys")) + .body( + serde_json::json!({ + "NetKeyVerify": { + "node": signer, + "signature": signature, + } + }) + .to_string(), + ) + .blob_bytes(message) + .send_and_await_response(10)??; + + let response: serde_json::Value = serde_json::from_slice(response.body())?; + let serde_json::Value::Bool(response) = response["NetKeyVerify"] else { + return Err(anyhow::anyhow!( + "unexpected response from sign:sign:sys: {response}" + )); + }; + + Ok(response) +} diff --git a/src/types/address.rs b/src/types/address.rs index f16dd3e..2b2022a 100644 --- a/src/types/address.rs +++ b/src/types/address.rs @@ -2,7 +2,7 @@ pub use crate::{Address, ProcessId, Request}; use serde::{Deserialize, Serialize}; use std::hash::{Hash, Hasher}; -/// Address is defined in `kinode.wit`, but constructors and methods here. +/// Address is defined in `hyperware.wit`, but constructors and methods here. /// An Address is a combination of a node ID (string) and a [`ProcessId`]. It is /// used in the [`Request`]/[`crate::Response`] pattern to indicate which process on a given node /// in the network to direct the message to. The formatting structure for diff --git a/src/types/lazy_load_blob.rs b/src/types/lazy_load_blob.rs index 1918639..3aa833d 100644 --- a/src/types/lazy_load_blob.rs +++ b/src/types/lazy_load_blob.rs @@ -1,10 +1,17 @@ +use std::fmt; +use std::marker::PhantomData; + +use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _}; +use serde::de::{self, Visitor}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; + pub use crate::LazyLoadBlob; /// `LazyLoadBlob` is defined in the wit bindings, but constructors and methods here. /// A `LazyLoadBlob` is a piece of data that is only optionally loaded into a process /// (i.e. with `get_blob()`). `LazyLoadBlob` is useful for passing large data in a chain /// of [`crate::Request`]s or [`crate::Response`]s where intermediate processes in the -/// chain don't need to access the data. In this way, Kinode saves time and compute +/// chain don't need to access the data. In this way, Hyperware saves time and compute /// since the `LazyLoadBlob` is not sent back and forth across the Wasm boundary needlessly. impl LazyLoadBlob { /// Create a new `LazyLoadBlob`. Takes a mime type and a byte vector. @@ -42,3 +49,94 @@ impl std::cmp::PartialEq for LazyLoadBlob { self.mime == other.mime && self.bytes == other.bytes } } + +impl Serialize for LazyLoadBlob { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + // Create a struct with 2 fields + use serde::ser::SerializeStruct; + let mut state = serializer.serialize_struct("LazyLoadBlob", 2)?; + + // Serialize mime normally (serde handles Option automatically) + state.serialize_field("mime", &self.mime)?; + + let base64_data = BASE64.encode(&self.bytes); + state.serialize_field("bytes", &base64_data)?; + + state.end() + } +} + +// Custom visitor for deserialization +struct LazyLoadBlobVisitor { + marker: PhantomData LazyLoadBlob>, +} + +impl LazyLoadBlobVisitor { + fn new() -> Self { + LazyLoadBlobVisitor { + marker: PhantomData, + } + } +} + +impl<'de> Visitor<'de> for LazyLoadBlobVisitor { + type Value = LazyLoadBlob; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a struct with mime and bytes fields") + } + + fn visit_map(self, mut map: M) -> Result + where + M: de::MapAccess<'de>, + { + let mut mime = None; + let mut bytes_base64 = None; + + // Extract each field from the map + while let Some(key) = map.next_key::()? { + match key.as_str() { + "mime" => { + if mime.is_some() { + return Err(de::Error::duplicate_field("mime")); + } + mime = map.next_value()?; + } + "bytes" => { + if bytes_base64.is_some() { + return Err(de::Error::duplicate_field("bytes")); + } + bytes_base64 = Some(map.next_value::()?); + } + _ => { + // Skip unknown fields + let _ = map.next_value::()?; + } + } + } + + let bytes_base64 = bytes_base64.ok_or_else(|| de::Error::missing_field("bytes"))?; + + let bytes = BASE64 + .decode(bytes_base64.as_bytes()) + .map_err(|err| de::Error::custom(format!("Invalid base64: {}", err)))?; + + Ok(LazyLoadBlob { mime, bytes }) + } +} + +impl<'de> Deserialize<'de> for LazyLoadBlob { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_struct( + "LazyLoadBlob", + &["mime", "bytes"], + LazyLoadBlobVisitor::new(), + ) + } +} diff --git a/src/types/message.rs b/src/types/message.rs index 45b0c5f..6d0b515 100644 --- a/src/types/message.rs +++ b/src/types/message.rs @@ -27,7 +27,7 @@ pub enum Message { }, } -#[derive(Debug, Error, Serialize, Deserialize)] +#[derive(Clone, Debug, Error, Serialize, Deserialize)] pub enum BuildError { #[error("no body set for message")] NoBody, @@ -105,22 +105,24 @@ impl Message { pub fn _wit_message_to_message( source: Address, - message: crate::kinode::process::standard::Message, + message: crate::hyperware::process::standard::Message, ) -> Message { match message { - crate::kinode::process::standard::Message::Request(req) => Message::Request { + crate::hyperware::process::standard::Message::Request(req) => Message::Request { source, expects_response: req.expects_response, body: req.body, metadata: req.metadata, capabilities: req.capabilities, }, - crate::kinode::process::standard::Message::Response((resp, context)) => Message::Response { - source, - body: resp.body, - metadata: resp.metadata, - context, - capabilities: resp.capabilities, - }, + crate::hyperware::process::standard::Message::Response((resp, context)) => { + Message::Response { + source, + body: resp.body, + metadata: resp.metadata, + context, + capabilities: resp.capabilities, + } + } } } diff --git a/src/types/on_exit.rs b/src/types/on_exit.rs index d578309..db4c6cf 100644 --- a/src/types/on_exit.rs +++ b/src/types/on_exit.rs @@ -10,10 +10,10 @@ pub enum OnExit { impl OnExit { /// Call the kernel to get the current set `OnExit` behavior. pub fn get() -> Self { - match crate::kinode::process::standard::get_on_exit() { - crate::kinode::process::standard::OnExit::None => OnExit::None, - crate::kinode::process::standard::OnExit::Restart => OnExit::Restart, - crate::kinode::process::standard::OnExit::Requests(reqs) => { + match crate::hyperware::process::standard::get_on_exit() { + crate::hyperware::process::standard::OnExit::None => OnExit::None, + crate::hyperware::process::standard::OnExit::Restart => OnExit::Restart, + crate::hyperware::process::standard::OnExit::Requests(reqs) => { let mut requests: Vec = Vec::with_capacity(reqs.len()); for req in reqs { requests.push(Request { @@ -74,27 +74,27 @@ impl OnExit { /// Will return a [`BuildError`] if any requests within the [`OnExit::Requests`] behavior are /// not valid (by not having a `body` and/or `target` set). pub fn set(self) -> Result<(), BuildError> { - crate::kinode::process::standard::set_on_exit(&self._to_standard()?); + crate::hyperware::process::standard::set_on_exit(&self._to_standard()?); Ok(()) } /// Convert this `OnExit` to the kernel's `OnExit` type. /// /// Will return a [`BuildError`] if any requests within the [`OnExit::Requests`] behavior are /// not valid (by not having a `body` and/or `target` set). - pub fn _to_standard(self) -> Result { + pub fn _to_standard(self) -> Result { match self { - OnExit::None => Ok(crate::kinode::process::standard::OnExit::None), - OnExit::Restart => Ok(crate::kinode::process::standard::OnExit::Restart), + OnExit::None => Ok(crate::hyperware::process::standard::OnExit::None), + OnExit::Restart => Ok(crate::hyperware::process::standard::OnExit::Restart), OnExit::Requests(reqs) => { let mut kernel_reqs: Vec<( Address, - crate::kinode::process::standard::Request, + crate::hyperware::process::standard::Request, Option, )> = Vec::with_capacity(reqs.len()); for req in reqs { kernel_reqs.push(( req.target.ok_or(BuildError::NoTarget)?, - crate::kinode::process::standard::Request { + crate::hyperware::process::standard::Request { inherit: req.inherit, expects_response: None, body: req.body.ok_or(BuildError::NoBody)?, @@ -104,7 +104,7 @@ impl OnExit { req.blob, )); } - Ok(crate::kinode::process::standard::OnExit::Requests( + Ok(crate::hyperware::process::standard::OnExit::Requests( kernel_reqs, )) } diff --git a/src/types/request.rs b/src/types/request.rs index b5e0649..f8435eb 100644 --- a/src/types/request.rs +++ b/src/types/request.rs @@ -273,7 +273,7 @@ impl Request { }; crate::send_request( &target, - &crate::kinode::process::standard::Request { + &crate::hyperware::process::standard::Request { inherit: self.inherit, expects_response: self.timeout, body, @@ -299,7 +299,7 @@ impl Request { }; match crate::send_and_await_response( &target, - &crate::kinode::process::standard::Request { + &crate::hyperware::process::standard::Request { inherit: self.inherit, expects_response: Some(timeout), body, diff --git a/src/types/response.rs b/src/types/response.rs index fe8a035..dabc86a 100644 --- a/src/types/response.rs +++ b/src/types/response.rs @@ -166,7 +166,7 @@ impl Response { pub fn send(self) -> Result<(), BuildError> { if let Some(body) = self.body { crate::send_response( - &crate::kinode::process::standard::Response { + &crate::hyperware::process::standard::Response { inherit: self.inherit, body, metadata: self.metadata, diff --git a/src/types/send_error.rs b/src/types/send_error.rs index fe9d2d7..acf4ee9 100644 --- a/src/types/send_error.rs +++ b/src/types/send_error.rs @@ -1,7 +1,7 @@ use crate::{Address, LazyLoadBlob, Message, _wit_message_to_message}; use serde::{Deserialize, Serialize}; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Deserialize, Serialize)] pub struct SendError { pub kind: SendErrorKind, pub target: Address, @@ -62,13 +62,13 @@ impl SendErrorKind { } pub fn _wit_send_error_to_send_error( - send_err: crate::kinode::process::standard::SendError, + send_err: crate::hyperware::process::standard::SendError, context: Option>, ) -> SendError { SendError { kind: match send_err.kind { - crate::kinode::process::standard::SendErrorKind::Offline => SendErrorKind::Offline, - crate::kinode::process::standard::SendErrorKind::Timeout => SendErrorKind::Timeout, + crate::hyperware::process::standard::SendErrorKind::Offline => SendErrorKind::Offline, + crate::hyperware::process::standard::SendErrorKind::Timeout => SendErrorKind::Timeout, }, target: send_err.target.clone(), message: _wit_message_to_message(send_err.target, send_err.message), diff --git a/src/vfs/file.rs b/src/vfs/file.rs index a0b25ed..4a2b716 100644 --- a/src/vfs/file.rs +++ b/src/vfs/file.rs @@ -6,6 +6,7 @@ use crate::{get_blob, PackageId}; /// VFS (Virtual File System) helper struct for a file. /// Opening or creating a `File` will give you a `Result`. /// You can call its impl functions to interact with it. +#[derive(serde::Deserialize, serde::Serialize)] pub struct File { pub path: String, pub timeout: u64,