Skip to main content

kinode_process_lib/
kimap.rs

1use crate::eth::{EthError, Provider};
2use crate::kimap::contract::getCall;
3use crate::net;
4use alloy::rpc::types::request::{TransactionInput, TransactionRequest};
5use alloy::{hex, primitives::keccak256};
6use alloy_primitives::{Address, Bytes, FixedBytes, B256};
7use alloy_sol_types::{SolCall, SolEvent, SolValue};
8use contract::tokenCall;
9use serde::{Deserialize, Serialize};
10use std::error::Error;
11use std::fmt;
12use std::str::FromStr;
13
14/// kimap deployment address on base
15pub const KIMAP_ADDRESS: &'static str = "0x000000000033e5CCbC52Ec7BDa87dB768f9aA93F";
16/// base chain id
17pub const KIMAP_CHAIN_ID: u64 = 8453;
18/// first block (minus one) of kimap deployment on base
19pub const KIMAP_FIRST_BLOCK: u64 = 25_346_377;
20/// the root hash of kimap, empty bytes32
21pub const KIMAP_ROOT_HASH: &'static str =
22    "0x0000000000000000000000000000000000000000000000000000000000000000";
23
24/// Sol structures for Kimap requests
25pub mod contract {
26    use alloy_sol_macro::sol;
27
28    sol! {
29        /// Emitted when a new namespace entry is minted.
30        /// - parenthash: The hash of the parent namespace entry.
31        /// - childhash: The hash of the minted namespace entry's full path.
32        /// - labelhash: The hash of only the label (the final entry in the path).
33        /// - label: The label (the final entry in the path) of the new entry.
34        event Mint(
35            bytes32 indexed parenthash,
36            bytes32 indexed childhash,
37            bytes indexed labelhash,
38            bytes label
39        );
40
41        /// Emitted when a fact is created on an existing namespace entry.
42        /// Facts are immutable and may only be written once. A fact label is
43        /// prepended with an exclamation mark (!) to indicate that it is a fact.
44        /// - parenthash The hash of the parent namespace entry.
45        /// - facthash The hash of the newly created fact's full path.
46        /// - labelhash The hash of only the label (the final entry in the path).
47        /// - label The label of the fact.
48        /// - data The data stored at the fact.
49        event Fact(
50            bytes32 indexed parenthash,
51            bytes32 indexed facthash,
52            bytes indexed labelhash,
53            bytes label,
54            bytes data
55        );
56
57        /// Emitted when a new note is created on an existing namespace entry.
58        /// Notes are mutable. A note label is prepended with a tilde (~) to indicate
59        /// that it is a note.
60        /// - parenthash: The hash of the parent namespace entry.
61        /// - notehash: The hash of the newly created note's full path.
62        /// - labelhash: The hash of only the label (the final entry in the path).
63        /// - label: The label of the note.
64        /// - data: The data stored at the note.
65        event Note(
66            bytes32 indexed parenthash,
67            bytes32 indexed notehash,
68            bytes indexed labelhash,
69            bytes label,
70            bytes data
71        );
72
73        /// Emitted when a gene is set for an existing namespace entry.
74        /// A gene is a specific TBA implementation which will be applied to all
75        /// sub-entries of the namespace entry.
76        /// - entry: The namespace entry's namehash.
77        /// - gene: The address of the TBA implementation.
78        event Gene(bytes32 indexed entry, address indexed gene);
79
80        /// Emitted when the zeroth namespace entry is minted.
81        /// Occurs exactly once at initialization.
82        /// - zeroTba: The address of the zeroth TBA
83        event Zero(address indexed zeroTba);
84
85        /// Emitted when a namespace entry is transferred from one address
86        /// to another.
87        /// - from: The address of the sender.
88        /// - to: The address of the recipient.
89        /// - id: The namehash of the namespace entry (converted to uint256).
90        event Transfer(
91            address indexed from,
92            address indexed to,
93            uint256 indexed id
94        );
95
96        /// Emitted when a namespace entry is approved for transfer.
97        /// - owner: The address of the owner.
98        /// - spender: The address of the spender.
99        /// - id: The namehash of the namespace entry (converted to uint256).
100        event Approval(
101            address indexed owner,
102            address indexed spender,
103            uint256 indexed id
104        );
105
106        /// Emitted when an operator is approved for all of an owner's
107        /// namespace entries.
108        /// - owner: The address of the owner.
109        /// - operator: The address of the operator.
110        /// - approved: Whether the operator is approved.
111        event ApprovalForAll(
112            address indexed owner,
113            address indexed operator,
114            bool approved
115        );
116
117        /// Retrieves information about a specific namespace entry.
118        /// - namehash The namehash of the namespace entry to query.
119        ///
120        /// Returns:
121        /// - tba: The address of the token-bound account associated
122        /// with the entry.
123        /// - owner: The address of the entry owner.
124        /// - data: The note or fact bytes associated with the entry
125        /// (empty if not a note or fact).
126        function get(
127            bytes32 namehash
128        ) external view returns (address tba, address owner, bytes memory data);
129
130        /// Mints a new namespace entry and creates a token-bound account for
131        /// it. Must be called by a parent namespace entry token-bound account.
132        /// - who: The address to own the new namespace entry.
133        /// - label: The label to mint beneath the calling parent entry.
134        /// - initialization: Initialization calldata applied to the new
135        /// minted entry's token-bound account.
136        /// - erc721Data: ERC-721 data -- passed to comply with
137        /// `ERC721TokenReceiver.onERC721Received()`.
138        /// - implementation: The address of the implementation contract for
139        /// the token-bound account: this will be overriden by the gene if the
140        /// parent entry has one set.
141        ///
142        /// Returns:
143        /// - tba: The address of the new entry's token-bound account.
144        function mint(
145            address who,
146            bytes calldata label,
147            bytes calldata initialization,
148            bytes calldata erc721Data,
149            address implementation
150        ) external returns (address tba);
151
152        /// Sets the gene for the calling namespace entry.
153        /// - _gene: The address of the TBA implementation to set for all
154        /// children of the calling namespace entry.
155        function gene(address _gene) external;
156
157        /// Creates a new fact beneath the calling namespace entry.
158        /// - fact: The fact label to create. Must be prepended with an
159        /// exclamation mark (!).
160        /// - data: The data to be stored at the fact.
161        ///
162        /// Returns:
163        /// - facthash: The namehash of the newly created fact.
164        function fact(
165            bytes calldata fact,
166            bytes calldata data
167        ) external returns (bytes32 facthash);
168
169        /// Creates a new note beneath the calling namespace entry.
170        /// - note: The note label to create. Must be prepended with a tilde (~).
171        /// - data: The data to be stored at the note.
172        ///
173        /// Returns:
174        /// - notehash: The namehash of the newly created note.
175        function note(
176            bytes calldata note,
177            bytes calldata data
178        ) external returns (bytes32 notehash);
179
180        /// Retrieves the token-bound account address of a namespace entry.
181        /// - entry: The entry namehash (as uint256) for which to get the
182        /// token-bound account.
183        ///
184        /// Returns:
185        /// - tba: The token-bound account address of the namespace entry.
186        function tbaOf(uint256 entry) external view returns (address tba);
187
188        function balanceOf(address owner) external view returns (uint256);
189
190        function getApproved(uint256 entry) external view returns (address);
191
192        function isApprovedForAll(
193            address owner,
194            address operator
195        ) external view returns (bool);
196
197        function ownerOf(uint256 entry) external view returns (address);
198
199        function setApprovalForAll(address operator, bool approved) external;
200
201        function approve(address spender, uint256 entry) external;
202
203        function safeTransferFrom(address from, address to, uint256 id) external;
204
205        function safeTransferFrom(
206            address from,
207            address to,
208            uint256 id,
209            bytes calldata data
210        ) external;
211
212        function transferFrom(address from, address to, uint256 id) external;
213
214        function supportsInterface(bytes4 interfaceId) external view returns (bool);
215
216        /// Gets the token identifier that owns this token-bound account (TBA).
217        /// This is a core function of the ERC-6551 standard that returns the
218        /// identifying information about the NFT that owns this account.
219        /// The return values are constant and cannot change over time.
220        ///
221        /// Returns:
222        /// - chainId: The EIP-155 chain ID where the owning NFT exists
223        /// - tokenContract: The contract address of the owning NFT
224        /// - tokenId: The token ID of the owning NFT
225        function token()
226            external
227            view
228            returns (uint256 chainId, address tokenContract, uint256 tokenId);
229    }
230}
231
232/// A mint log from the kimap, converted to a 'resolved' format using
233/// namespace data saved in the kns-indexer.
234#[derive(Clone, Debug, Deserialize, Serialize)]
235pub struct Mint {
236    pub name: String,
237    pub parent_path: String,
238}
239
240/// A note log from the kimap, converted to a 'resolved' format using
241/// namespace data saved in the kns-indexer
242#[derive(Clone, Debug, Deserialize, Serialize)]
243pub struct Note {
244    pub note: String,
245    pub parent_path: String,
246    pub data: Bytes,
247}
248
249/// A fact log from the kimap, converted to a 'resolved' format using
250/// namespace data saved in the kns-indexer
251#[derive(Clone, Debug, Deserialize, Serialize)]
252pub struct Fact {
253    pub fact: String,
254    pub parent_path: String,
255    pub data: Bytes,
256}
257
258/// Errors that can occur when decoding a log from the kimap using
259/// [`decode_mint_log()`] or [`decode_note_log()`].
260#[derive(Clone, Debug, Deserialize, Serialize)]
261pub enum DecodeLogError {
262    /// The log's topic is not a mint or note event.
263    UnexpectedTopic(B256),
264    /// The name is not valid (according to [`valid_name`]).
265    InvalidName(String),
266    /// An error occurred while decoding the log.
267    DecodeError(String),
268    /// The parent name could not be resolved with `kns-indexer`.
269    UnresolvedParent(String),
270}
271
272impl fmt::Display for DecodeLogError {
273    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
274        match self {
275            DecodeLogError::UnexpectedTopic(topic) => write!(f, "Unexpected topic: {:?}", topic),
276            DecodeLogError::InvalidName(name) => write!(f, "Invalid name: {}", name),
277            DecodeLogError::DecodeError(err) => write!(f, "Decode error: {}", err),
278            DecodeLogError::UnresolvedParent(parent) => {
279                write!(f, "Could not resolve parent: {}", parent)
280            }
281        }
282    }
283}
284
285impl Error for DecodeLogError {}
286
287/// Canonical function to determine if a kimap entry is valid. This should
288/// be used whenever reading a new kimap entry from a mints query, because
289/// while most frontends will enforce these rules, it is possible to post
290/// invalid names to the kimap contract.
291///
292/// This checks a **single name**, not the full path-name. A full path-name
293/// is comprised of valid names separated by `.`
294pub fn valid_entry(entry: &str, note: bool, fact: bool) -> bool {
295    if note && fact {
296        return false;
297    }
298    if note {
299        valid_note(entry)
300    } else if fact {
301        valid_fact(entry)
302    } else {
303        valid_name(entry)
304    }
305}
306
307pub fn valid_name(name: &str) -> bool {
308    name.is_ascii()
309        && name.len() >= 1
310        && name
311            .chars()
312            .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
313}
314
315pub fn valid_note(note: &str) -> bool {
316    note.is_ascii()
317        && note.len() >= 2
318        && note.chars().next() == Some('~')
319        && note
320            .chars()
321            .skip(1)
322            .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
323}
324
325pub fn valid_fact(fact: &str) -> bool {
326    fact.is_ascii()
327        && fact.len() >= 2
328        && fact.chars().next() == Some('!')
329        && fact
330            .chars()
331            .skip(1)
332            .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
333}
334
335/// Produce a namehash from a kimap name.
336pub fn namehash(name: &str) -> String {
337    let mut node = B256::default();
338
339    let mut labels: Vec<&str> = name.split('.').collect();
340    labels.reverse();
341
342    for label in labels.iter() {
343        let l = keccak256(label);
344        node = keccak256((node, l).abi_encode_packed());
345    }
346    format!("0x{}", hex::encode(node))
347}
348
349/// Decode a mint log from the kimap into a 'resolved' format.
350///
351/// Uses [`valid_name()`] to check if the name is valid.
352pub fn decode_mint_log(log: &crate::eth::Log) -> Result<Mint, DecodeLogError> {
353    let contract::Note::SIGNATURE_HASH = log.topics()[0] else {
354        return Err(DecodeLogError::UnexpectedTopic(log.topics()[0]));
355    };
356    let decoded = contract::Mint::decode_log_data(log.data(), true)
357        .map_err(|e| DecodeLogError::DecodeError(e.to_string()))?;
358    let name = String::from_utf8_lossy(&decoded.label).to_string();
359    if !valid_name(&name) {
360        return Err(DecodeLogError::InvalidName(name));
361    }
362    match resolve_parent(log, None) {
363        Some(parent_path) => Ok(Mint { name, parent_path }),
364        None => Err(DecodeLogError::UnresolvedParent(name)),
365    }
366}
367
368/// Decode a note log from the kimap into a 'resolved' format.
369///
370/// Uses [`valid_name()`] to check if the name is valid.
371pub fn decode_note_log(log: &crate::eth::Log) -> Result<Note, DecodeLogError> {
372    let contract::Note::SIGNATURE_HASH = log.topics()[0] else {
373        return Err(DecodeLogError::UnexpectedTopic(log.topics()[0]));
374    };
375    let decoded = contract::Note::decode_log_data(log.data(), true)
376        .map_err(|e| DecodeLogError::DecodeError(e.to_string()))?;
377    let note = String::from_utf8_lossy(&decoded.label).to_string();
378    if !valid_note(&note) {
379        return Err(DecodeLogError::InvalidName(note));
380    }
381    match resolve_parent(log, None) {
382        Some(parent_path) => Ok(Note {
383            note,
384            parent_path,
385            data: decoded.data,
386        }),
387        None => Err(DecodeLogError::UnresolvedParent(note)),
388    }
389}
390
391pub fn decode_fact_log(log: &crate::eth::Log) -> Result<Fact, DecodeLogError> {
392    let contract::Fact::SIGNATURE_HASH = log.topics()[0] else {
393        return Err(DecodeLogError::UnexpectedTopic(log.topics()[0]));
394    };
395    let decoded = contract::Fact::decode_log_data(log.data(), true)
396        .map_err(|e| DecodeLogError::DecodeError(e.to_string()))?;
397    let fact = String::from_utf8_lossy(&decoded.label).to_string();
398    if !valid_fact(&fact) {
399        return Err(DecodeLogError::InvalidName(fact));
400    }
401    match resolve_parent(log, None) {
402        Some(parent_path) => Ok(Fact {
403            fact,
404            parent_path,
405            data: decoded.data,
406        }),
407        None => Err(DecodeLogError::UnresolvedParent(fact)),
408    }
409}
410
411/// Given a [`crate::eth::Log`] (which must be a log from kimap), resolve the parent name
412/// of the new entry or note.
413pub fn resolve_parent(log: &crate::eth::Log, timeout: Option<u64>) -> Option<String> {
414    let parent_hash = log.topics()[1].to_string();
415    net::get_name(&parent_hash, log.block_number, timeout)
416}
417
418/// Given a [`crate::eth::Log`] (which must be a log from kimap), resolve the full name
419/// of the new entry or note.
420///
421/// Uses [`valid_name()`] to check if the name is valid.
422pub fn resolve_full_name(log: &crate::eth::Log, timeout: Option<u64>) -> Option<String> {
423    let parent_hash = log.topics()[1].to_string();
424    let parent_name = net::get_name(&parent_hash, log.block_number, timeout)?;
425    let log_name = match log.topics()[0] {
426        contract::Mint::SIGNATURE_HASH => {
427            let decoded = contract::Mint::decode_log_data(log.data(), true).unwrap();
428            decoded.label
429        }
430        contract::Note::SIGNATURE_HASH => {
431            let decoded = contract::Note::decode_log_data(log.data(), true).unwrap();
432            decoded.label
433        }
434        contract::Fact::SIGNATURE_HASH => {
435            let decoded = contract::Fact::decode_log_data(log.data(), true).unwrap();
436            decoded.label
437        }
438        _ => return None,
439    };
440    let name = String::from_utf8_lossy(&log_name);
441    if !valid_entry(
442        &name,
443        log.topics()[0] == contract::Note::SIGNATURE_HASH,
444        log.topics()[0] == contract::Fact::SIGNATURE_HASH,
445    ) {
446        return None;
447    }
448    Some(format!("{name}.{parent_name}"))
449}
450
451/// Helper struct for reading from the kimap.
452#[derive(Clone, Debug, Deserialize, Serialize)]
453pub struct Kimap {
454    pub provider: Provider,
455    address: Address,
456}
457
458impl Kimap {
459    /// Creates a new Kimap instance with a specified address.
460    ///
461    /// # Arguments
462    /// * `provider` - A reference to the Provider.
463    /// * `address` - The address of the Kimap contract.
464    pub fn new(provider: Provider, address: Address) -> Self {
465        Self { provider, address }
466    }
467
468    /// Creates a new Kimap instance with the default address and chain ID.
469    pub fn default(timeout: u64) -> Self {
470        let provider = Provider::new(KIMAP_CHAIN_ID, timeout);
471        Self::new(provider, Address::from_str(KIMAP_ADDRESS).unwrap())
472    }
473
474    /// Returns the in-use Kimap contract address.
475    pub fn address(&self) -> &Address {
476        &self.address
477    }
478
479    /// Gets an entry from the Kimap by its string-formatted name.
480    ///
481    /// # Parameters
482    /// - `path`: The name-path to get from the Kimap.
483    /// # Returns
484    /// A `Result<(Address, Address, Option<Bytes>), EthError>` representing the TBA, owner,
485    /// and value if the entry exists and is a note.
486    pub fn get(&self, path: &str) -> Result<(Address, Address, Option<Bytes>), EthError> {
487        let get_call = getCall {
488            namehash: FixedBytes::<32>::from_str(&namehash(path))
489                .map_err(|_| EthError::InvalidParams)?,
490        }
491        .abi_encode();
492
493        let tx_req = TransactionRequest::default()
494            .input(TransactionInput::new(get_call.into()))
495            .to(self.address);
496
497        let res_bytes = self.provider.call(tx_req, None)?;
498
499        let res = getCall::abi_decode_returns(&res_bytes, false)
500            .map_err(|_| EthError::RpcMalformedResponse)?;
501
502        let note_data = if res.data == Bytes::default() {
503            None
504        } else {
505            Some(res.data)
506        };
507
508        Ok((res.tba, res.owner, note_data))
509    }
510
511    /// Gets an entry from the Kimap by its hash.
512    ///
513    /// # Parameters
514    /// - `entryhash`: The entry to get from the Kimap.
515    /// # Returns
516    /// A `Result<(Address, Address, Option<Bytes>), EthError>` representing the TBA, owner,
517    /// and value if the entry exists and is a note.
518    pub fn get_hash(&self, entryhash: &str) -> Result<(Address, Address, Option<Bytes>), EthError> {
519        let get_call = getCall {
520            namehash: FixedBytes::<32>::from_str(entryhash).map_err(|_| EthError::InvalidParams)?,
521        }
522        .abi_encode();
523
524        let tx_req = TransactionRequest::default()
525            .input(TransactionInput::new(get_call.into()))
526            .to(self.address);
527
528        let res_bytes = self.provider.call(tx_req, None)?;
529
530        let res = getCall::abi_decode_returns(&res_bytes, false)
531            .map_err(|_| EthError::RpcMalformedResponse)?;
532
533        let note_data = if res.data == Bytes::default() {
534            None
535        } else {
536            Some(res.data)
537        };
538
539        Ok((res.tba, res.owner, note_data))
540    }
541
542    /// Gets a namehash from an existing TBA address.
543    ///
544    /// # Parameters
545    /// - `tba`: The TBA to get the namehash of.
546    /// # Returns
547    /// A `Result<String, EthError>` representing the namehash of the TBA.
548    pub fn get_namehash_from_tba(&self, tba: Address) -> Result<String, EthError> {
549        let token_call = tokenCall {}.abi_encode();
550
551        let tx_req = TransactionRequest::default()
552            .input(TransactionInput::new(token_call.into()))
553            .to(tba);
554
555        let res_bytes = self.provider.call(tx_req, None)?;
556
557        let res = tokenCall::abi_decode_returns(&res_bytes, false)
558            .map_err(|_| EthError::RpcMalformedResponse)?;
559
560        let namehash: FixedBytes<32> = res.tokenId.into();
561        Ok(format!("0x{}", hex::encode(namehash)))
562    }
563
564    /// Create a filter for all mint events.
565    pub fn mint_filter(&self) -> crate::eth::Filter {
566        crate::eth::Filter::new()
567            .address(self.address)
568            .event(contract::Mint::SIGNATURE)
569    }
570
571    /// Create a filter for all note events.
572    pub fn note_filter(&self) -> crate::eth::Filter {
573        crate::eth::Filter::new()
574            .address(self.address)
575            .event(contract::Note::SIGNATURE)
576    }
577
578    /// Create a filter for all fact events.
579    pub fn fact_filter(&self) -> crate::eth::Filter {
580        crate::eth::Filter::new()
581            .address(self.address)
582            .event(contract::Fact::SIGNATURE)
583    }
584
585    /// Create a filter for a given set of specific notes. This function will
586    /// hash the note labels and use them as the topic3 filter.
587    ///
588    /// Example:
589    /// ```rust
590    /// let filter = kimap.notes_filter(&["~note1", "~note2"]);
591    /// ```
592    pub fn notes_filter(&self, notes: &[&str]) -> crate::eth::Filter {
593        self.note_filter().topic3(
594            notes
595                .into_iter()
596                .map(|note| keccak256(note))
597                .collect::<Vec<_>>(),
598        )
599    }
600
601    /// Create a filter for a given set of specific facts. This function will
602    /// hash the fact labels and use them as the topic3 filter.
603    ///
604    /// Example:
605    /// ```rust
606    /// let filter = kimap.facts_filter(&["!fact1", "!fact2"]);
607    /// ```
608    pub fn facts_filter(&self, facts: &[&str]) -> crate::eth::Filter {
609        self.fact_filter().topic3(
610            facts
611                .into_iter()
612                .map(|fact| keccak256(fact))
613                .collect::<Vec<_>>(),
614        )
615    }
616}